├── .commitlintrc ├── docs ├── assets │ ├── logo.png │ └── favicon.png ├── dateutils │ ├── ceil.md │ ├── floor.md │ ├── overlap.md │ ├── afterorequal.md │ ├── beforeorequal.md │ └── index.md ├── stringutils │ ├── capitalize.md │ ├── padLeft.md │ ├── padRight.md │ ├── stringify.md │ └── index.md ├── ptrutils │ ├── isNil.md │ ├── toSlice.md │ ├── derefSlice.md │ ├── nonNilSlice.md │ ├── toPointerSlice.md │ ├── coalesce.md │ ├── deref.md │ ├── to.md │ ├── equal.md │ └── index.md ├── sliceutils │ ├── unique.md │ ├── sum.md │ ├── flatten.md │ ├── remove.md │ ├── reverse.md │ ├── union.md │ ├── copy.md │ ├── difference.md │ ├── intersection.md │ ├── takeLast.md │ ├── chunk.md │ ├── ensureUniqueAndAppend.md │ ├── tail.md │ ├── skipLast.md │ ├── head.md │ ├── initial.md │ ├── last.md │ ├── insert.md │ ├── includes.md │ ├── findIndexesOf.md │ ├── merge.md │ ├── take.md │ ├── takeWhile.md │ ├── findLastIndexOf.md │ ├── skip.md │ ├── forEach.md │ ├── skipWhile.md │ ├── filter.md │ ├── windows.md │ ├── findIndexOf.md │ ├── countBy.md │ ├── map.md │ ├── some.md │ ├── every.md │ ├── compact.md │ ├── flatMap.md │ ├── partition.md │ ├── pluck.md │ ├── minBy.md │ ├── maxBy.md │ ├── reduce.md │ ├── distinctBy.md │ ├── findIndexes.md │ ├── find.md │ ├── findLastIndex.md │ ├── findIndex.md │ ├── groupBy.md │ └── index.md ├── mathutils │ ├── abs.md │ ├── lcm.md │ ├── gcd.md │ ├── average.md │ ├── inRange.md │ ├── clamp.md │ ├── isPrime.md │ └── index.md ├── excutils │ ├── ignoreErr.md │ ├── catch.md │ ├── must.md │ ├── allErr.md │ ├── firstErr.md │ ├── mustResult.md │ ├── mustNotNil.md │ ├── returnAnyErr.md │ ├── recoverWithValue.md │ ├── try.md │ ├── retry.md │ ├── returnNotNil.md │ ├── retryWithResult.md │ └── index.md ├── maputils │ ├── values.md │ ├── keys.md │ ├── fromEntries.md │ ├── forEach.md │ ├── has.md │ ├── invert.md │ ├── toEntries.md │ ├── omit.md │ ├── pick.md │ ├── get.md │ ├── filter.md │ ├── map.md │ ├── copy.md │ ├── merge.md │ ├── mapKeys.md │ ├── groupBy.md │ ├── drop.md │ └── index.md ├── urlutils │ ├── queryStringifyMap.md │ ├── index.md │ └── queryStringifyStruct.md ├── structutils │ ├── forEach.md │ ├── index.md │ └── toMap.md ├── contributing.md ├── css │ └── extra.css └── index.md ├── SECURITY.md ├── .github ├── dependabot.yaml └── workflows │ ├── docs.yaml │ └── ci.yaml ├── .gitignore ├── go.mod ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── LICENSE ├── go.sum ├── CHANGELOG.md ├── mathutils ├── mathutils.go └── mathutils_test.go ├── ptrutils ├── ptrutils.go └── ptrutils_test.go ├── urlutils ├── urlutils.go └── urlutils_test.go ├── structutils ├── structutils.go └── structutils_test.go ├── .golangci.yml ├── CODE_OF_CONDUCT.md ├── README.md ├── excutils └── excutils.go ├── maputils ├── maputils_test.go └── maputils.go ├── dateutils └── dateutils.go ├── mkdocs.yml └── stringutils └── stringutils_test.go /.commitlintrc: -------------------------------------------------------------------------------- 1 | { "extends": ["@commitlint/config-conventional"] } 2 | -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goldziher/go-utils/HEAD/docs/assets/logo.png -------------------------------------------------------------------------------- /docs/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goldziher/go-utils/HEAD/docs/assets/favicon.png -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | To report a vulnerability create an issue on github. 6 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | .vscode/ 4 | node_modules/ 5 | 6 | # AI Rules generated files 7 | .claude/agents/ 8 | .cursor/rules/ 9 | .gemini/settings.json 10 | .github/copilot-instructions.md 11 | .mcp.json 12 | .windsurf/ 13 | AGENTS.md 14 | CLAUDE.md 15 | GEMINI.md 16 | 17 | # MkDocs 18 | site/ 19 | .cache/ 20 | .venv/ 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Goldziher/go-utils 2 | 3 | go 1.25 4 | 5 | require ( 6 | github.com/stretchr/testify v1.7.1 7 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.0 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /docs/dateutils/ceil.md: -------------------------------------------------------------------------------- 1 | # Ceil 2 | 3 | `func Ceil(t time.Time) time.Time` 4 | 5 | Ceil - takes a datetime and return a datetime from the same day at 23:59:59. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "github.com/Goldziher/go-utils/dateutils" 13 | "time" 14 | ) 15 | 16 | func main() { 17 | d, _ := time.Parse("2006-01-02", "2009-10-13") 18 | fmt.Println(dateutils.Ceil(d)) // 2009-10-13 23:59:59 +0000 UTC 19 | } 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/stringutils/capitalize.md: -------------------------------------------------------------------------------- 1 | # Capitalize 2 | 3 | `func Capitalize(str string) string` 4 | 5 | Capitalize - Capitalizes a string by changing the casing format of the first letter of the string. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "github.com/Goldziher/go-utils/stringutils" 13 | ) 14 | 15 | func main() { 16 | result := stringutils.Capitalize("coffee") // "Coffee" 17 | fmt.Print(result) // "Coffee" 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/dateutils/floor.md: -------------------------------------------------------------------------------- 1 | # Floor 2 | 3 | `func Floor(t time.Time) time.Time` 4 | 5 | Floor - takes a datetime and return a datetime from the same day at 00:00:00. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "github.com/Goldziher/go-utils/dateutils" 13 | "time" 14 | ) 15 | 16 | func main() { 17 | d, _ := time.Parse("2006-01-02", "2009-10-13") 18 | fmt.Println(dateutils.Floor(d)) // 2009-10-13 00:00:00 +0000 UTC 19 | } 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/ptrutils/isNil.md: -------------------------------------------------------------------------------- 1 | # IsNil 2 | 3 | `func IsNil[T any](ptr *T) bool` 4 | 5 | IsNil checks if a pointer is nil. This is a convenience function for readability. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | ptr "github.com/Goldziher/go-utils/ptrutils" 14 | ) 15 | 16 | func main() { 17 | numPtr := ptr.To(42) 18 | var nilPtr *int 19 | 20 | fmt.Println(ptr.IsNil(numPtr)) // false 21 | fmt.Println(ptr.IsNil(nilPtr)) // true 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/sliceutils/unique.md: -------------------------------------------------------------------------------- 1 | # Unique 2 | 3 | `func Unique[T comparable](slice []T) []T` 4 | 5 | Unique takes a slice of type T and returns a slice of type T containing all unique elements. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | numerals := []int{0, 1, 2, 3, 3, 1} 18 | 19 | result := sliceutils.Unique(numerals) 20 | 21 | fmt.Print(result) // [0, 1, 2, 3] 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/sliceutils/sum.md: -------------------------------------------------------------------------------- 1 | # Sum 2 | 3 | `func Sum[T numbers](slice []T) (result T)` 4 | 5 | Sum takes a slice of numbers T, which can be any of the number types, and returns a sum of their values. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | numerals := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 18 | 19 | result := sliceutils.Sum(numerals) 20 | 21 | fmt.Print(result) // 45 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/mathutils/abs.md: -------------------------------------------------------------------------------- 1 | # Abs 2 | 3 | `func Abs[T Number](value T) T` 4 | 5 | Abs returns the absolute value of a number. For unsigned types, returns the value as-is. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/mathutils" 14 | ) 15 | 16 | func main() { 17 | fmt.Println(mathutils.Abs(5)) // 5 18 | fmt.Println(mathutils.Abs(-5)) // 5 19 | fmt.Println(mathutils.Abs(0)) // 0 20 | fmt.Println(mathutils.Abs(-3.5)) // 3.5 21 | } 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/sliceutils/flatten.md: -------------------------------------------------------------------------------- 1 | # Flatten 2 | 3 | `Flatten[I any](input [][]I) []I` 4 | 5 | Flatten - receives a slice of slice of type I and flattens it to a slice of type I. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "github.com/Goldziher/go-utils/sliceutils" 12 | ) 13 | 14 | func main() { 15 | items := [][]int{ 16 | {1, 2, 3, 4}, 17 | {5, 6}, 18 | {7, 8}, 19 | {9, 10, 11}, 20 | } 21 | 22 | flattened := sliceutils.Flatten(items) //[]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/sliceutils/remove.md: -------------------------------------------------------------------------------- 1 | # Remove 2 | 3 | `func Remove[T any](slice []T, i int) []T` 4 | 5 | Remove takes a slice of type T and an index, removing the element at the given index. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | numerals := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 18 | 19 | numerals = sliceutils.Remove(numerals, 3) 20 | 21 | fmt.Print(numerals) // [0, 1, 2, 4, 5, 6, 7, 8, 9] 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/sliceutils/reverse.md: -------------------------------------------------------------------------------- 1 | # Reverse 2 | 3 | `func Reverse[T any](slice []T) []T` 4 | 5 | Reverse takes a slice of type T and returns a slice of type T with a reverse order of elements. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | numerals := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 18 | 19 | reversed := sliceutils.Reverse(numerals) 20 | 21 | fmt.Print(reversed) // [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/excutils/ignoreErr.md: -------------------------------------------------------------------------------- 1 | # IgnoreErr 2 | 3 | `func IgnoreErr(fn func() error)` 4 | 5 | IgnoreErr executes a function that returns an error and ignores the error. This is useful for cleanup operations where errors can be safely ignored. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "os" 12 | 13 | exc "github.com/Goldziher/go-utils/excutils" 14 | ) 15 | 16 | func main() { 17 | file, _ := os.Open("temp.txt") 18 | defer exc.IgnoreErr(file.Close) 19 | 20 | // Use file... 21 | // Close error will be ignored 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/mathutils/lcm.md: -------------------------------------------------------------------------------- 1 | # Lcm 2 | 3 | `func Lcm[T constraints.Integer](a, b T) T` 4 | 5 | Lcm returns the least common multiple of two integers. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/mathutils" 14 | ) 15 | 16 | func main() { 17 | fmt.Println(mathutils.Lcm(4, 6)) // 12 18 | fmt.Println(mathutils.Lcm(17, 13)) // 221 19 | fmt.Println(mathutils.Lcm(10, 5)) // 10 20 | fmt.Println(mathutils.Lcm(0, 5)) // 0 21 | fmt.Println(mathutils.Lcm(5, 0)) // 0 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/maputils/values.md: -------------------------------------------------------------------------------- 1 | # Values 2 | 3 | `func Values[K comparable, V any](mapInstance map[K]V) []V` 4 | 5 | Values takes a map with keys K and values V and returns a slice of type V with the map's values. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/maputils" 14 | ) 15 | 16 | func main() { 17 | vegetables := map[string]int{ 18 | "potatoes": 5, 19 | "carrots": 10, 20 | } 21 | 22 | result := maputils.Values(vegetables) 23 | 24 | fmt.Print(result) // [5, 10] 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/maputils/keys.md: -------------------------------------------------------------------------------- 1 | # Keys 2 | 3 | `func Keys[K comparable, V any](mapInstance map[K]V) []K` 4 | 5 | Keys takes a map with keys K and values V and returns a slice of type K with the map's keys. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/maputils" 14 | ) 15 | 16 | func main() { 17 | vegetables := map[string]int{ 18 | "potatoes": 5, 19 | "carrots": 10, 20 | } 21 | 22 | result := maputils.Keys(vegetables) 23 | 24 | fmt.Print(result) // ["potatoes", "carrots"] 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/sliceutils/union.md: -------------------------------------------------------------------------------- 1 | # Union 2 | 3 | `func Union[T comparable](slices ...[]T) []T` 4 | 5 | Union takes a variadic number of slices of type T and returns a slice of type T containing the unique elements in the 6 | different slices. 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | 14 | "github.com/Goldziher/go-utils/sliceutils" 15 | ) 16 | 17 | func main() { 18 | first, second := []int{1, 2, 3}, []int{1, 7, 3} 19 | 20 | result := sliceutils.Union(first, second) 21 | 22 | fmt.Print(result) // [1, 2, 3, 7] 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/sliceutils/copy.md: -------------------------------------------------------------------------------- 1 | # Copy 2 | 3 | `func Copy[T any](slice []T) []T` 4 | 5 | Copy receives a slice of type T and returns a copy. 6 | 7 | Deprecated: prefer `slices.Clone(slice)` from the standard library. 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | 15 | "github.com/Goldziher/go-utils/sliceutils" 16 | ) 17 | 18 | func main() { 19 | numerals := []int{0, 1} 20 | 21 | numeralsCopy := sliceutils.Copy(numerals) 22 | numeralsCopy[0] = 1 23 | 24 | fmt.Print(numerals) // [0, 1] 25 | fmt.Print(numeralsCopy) // [1, 1] 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/ptrutils/toSlice.md: -------------------------------------------------------------------------------- 1 | # ToSlice 2 | 3 | `func ToSlice[T any](ptr *T) []T` 4 | 5 | ToSlice converts a pointer to a slice. Returns an empty slice if ptr is nil, otherwise returns a slice with the single element. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | ptr "github.com/Goldziher/go-utils/ptrutils" 14 | ) 15 | 16 | func main() { 17 | numPtr := ptr.To(42) 18 | slice := ptr.ToSlice(numPtr) 19 | fmt.Println(slice) // [42] 20 | 21 | var nilPtr *int 22 | emptySlice := ptr.ToSlice(nilPtr) 23 | fmt.Println(emptySlice) // [] 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/sliceutils/difference.md: -------------------------------------------------------------------------------- 1 | # Difference 2 | 3 | `func Difference[T comparable](slices ...[]T) []T` 4 | 5 | Difference takes a variadic number of slices of type T and returns a slice of type T containing the elements that are 6 | different between the slices. 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | 14 | "github.com/Goldziher/go-utils/sliceutils" 15 | ) 16 | 17 | func main() { 18 | first, second := []int{1, 2, 3}, []int{1, 7, 3} 19 | 20 | result := sliceutils.Difference(first, second) 21 | 22 | fmt.Print(result) // [2, 7] 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/sliceutils/intersection.md: -------------------------------------------------------------------------------- 1 | # Intersection 2 | 3 | `func Intersection[T comparable](slices ...[]T) []T` 4 | 5 | Intersection takes a variadic number of slices of type T and returns a slice of type T containing any values that are 6 | common to all slices. 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | 14 | "github.com/Goldziher/go-utils/sliceutils" 15 | ) 16 | 17 | func main() { 18 | first, second := []int{1, 2, 3}, []int{1, 7, 3} 19 | 20 | result := sliceutils.Intersection(first, second) 21 | 22 | fmt.Print(result) // [1, 3] 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/sliceutils/takeLast.md: -------------------------------------------------------------------------------- 1 | # TakeLast 2 | 3 | `func TakeLast[T any](slice []T, n int) []T` 4 | 5 | TakeLast returns the last n elements from the slice. If n is greater than the slice length, returns the entire slice. If n is zero or negative, returns an empty slice. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 18 | 19 | last3 := sliceutils.TakeLast(numbers, 3) 20 | fmt.Printf("%v\n", last3) // [8 9 10] 21 | } 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/ptrutils/derefSlice.md: -------------------------------------------------------------------------------- 1 | # DerefSlice 2 | 3 | `func DerefSlice[T any](ptrs []*T, def T) []T` 4 | 5 | DerefSlice dereferences all pointers in a slice, using def for nil pointers. Returns a new slice with dereferenced values. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | ptr "github.com/Goldziher/go-utils/ptrutils" 14 | ) 15 | 16 | func main() { 17 | ptrs := []*int{ptr.To(1), nil, ptr.To(3), nil, ptr.To(5)} 18 | 19 | // Dereference with default value of 0 for nils 20 | values := ptr.DerefSlice(ptrs, 0) 21 | fmt.Println(values) // [1 0 3 0 5] 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/sliceutils/chunk.md: -------------------------------------------------------------------------------- 1 | # Chunk 2 | 3 | `func Chunk[T any](input []T, size int) [][]T ` 4 | 5 | Unique takes a slice of type T and size N and returns a slice of slices T of size N. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 18 | 19 | result1 := sliceutils.Chunk(numbers, 2) // [][]int{{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}} 20 | result2 := sliceutils.Chunk(numbers, 3) // [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10}} 21 | } 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/sliceutils/ensureUniqueAndAppend.md: -------------------------------------------------------------------------------- 1 | # EnsureUniqueAndAppend 2 | 3 | `func EnsureUniqueAndAppend[T comparable](slice []T, item T) []T` 4 | 5 | EnsureUniqueAndAppend appends an item to a slice if it does not already exists. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | slice := []string{} 18 | item := "go-utils" 19 | 20 | slice = sliceutils.EnsureUniqueAndAppend(slice, item) // ["go-utils"] 21 | slice = sliceutils.EnsureUniqueAndAppend(slice, item) // ["go-utils"] 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/mathutils/gcd.md: -------------------------------------------------------------------------------- 1 | # Gcd 2 | 3 | `func Gcd[T constraints.Integer](a, b T) T` 4 | 5 | Gcd returns the greatest common divisor of two integers using the Euclidean algorithm. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/mathutils" 14 | ) 15 | 16 | func main() { 17 | fmt.Println(mathutils.Gcd(48, 18)) // 6 18 | fmt.Println(mathutils.Gcd(17, 13)) // 1 19 | fmt.Println(mathutils.Gcd(10, 5)) // 5 20 | fmt.Println(mathutils.Gcd(-12, 24)) // 12 (always returns positive) 21 | fmt.Println(mathutils.Gcd(7, 0)) // 7 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/sliceutils/tail.md: -------------------------------------------------------------------------------- 1 | # Tail 2 | 3 | `func Tail[T any](slice []T) []T` 4 | 5 | Tail returns all elements of the slice except the first. Returns an empty slice if the slice has 0 or 1 elements. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | numbers := []int{1, 2, 3, 4, 5} 18 | 19 | rest := sliceutils.Tail(numbers) 20 | fmt.Printf("%v\n", rest) // [2 3 4 5] 21 | 22 | single := []int{1} 23 | emptyTail := sliceutils.Tail(single) 24 | fmt.Printf("%v\n", emptyTail) // [] 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_run: 3 | workflows: ["ci"] 4 | branches: [main] 5 | types: 6 | - completed 7 | jobs: 8 | docs: 9 | runs-on: ubuntu-latest 10 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 11 | steps: 12 | - uses: actions/checkout@v6 13 | with: 14 | fetch-depth: 0 15 | - uses: actions/setup-python@v6 16 | with: 17 | python-version: 3.14 18 | - run: pip install mkdocs-material "mkdocs-material[imaging]" mkdocs-git-revision-date-localized-plugin 19 | - run: mkdocs gh-deploy --force 20 | -------------------------------------------------------------------------------- /docs/excutils/catch.md: -------------------------------------------------------------------------------- 1 | # Catch 2 | 3 | `func Catch(fn func() error) (err error)` 4 | 5 | Catch executes a function and recovers from any panics, converting them to errors. Returns the function's error if any, or an error created from a recovered panic. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | exc "github.com/Goldziher/go-utils/excutils" 14 | ) 15 | 16 | func main() { 17 | err := exc.Catch(func() error { 18 | // This might panic 19 | panic("something went wrong") 20 | }) 21 | 22 | fmt.Printf("Caught: %v\n", err) 23 | // Caught: panic: something went wrong 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/mathutils/average.md: -------------------------------------------------------------------------------- 1 | # Average 2 | 3 | `func Average[T Number](values []T) float64` 4 | 5 | Average returns the average (mean) of all values in a slice. Returns 0 for empty slice. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/mathutils" 14 | ) 15 | 16 | func main() { 17 | integers := []int{1, 2, 3, 4, 5} 18 | fmt.Println(mathutils.Average(integers)) // 3.0 19 | 20 | floats := []float64{2.5, 7.5} 21 | fmt.Println(mathutils.Average(floats)) // 5.0 22 | 23 | empty := []int{} 24 | fmt.Println(mathutils.Average(empty)) // 0.0 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/sliceutils/skipLast.md: -------------------------------------------------------------------------------- 1 | # SkipLast 2 | 3 | `func SkipLast[T any](slice []T, n int) []T` 4 | 5 | SkipLast returns the slice with the last n elements removed. If n is greater than or equal to the slice length, returns an empty slice. If n is zero or negative, returns the original slice. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 18 | 19 | skipLast3 := sliceutils.SkipLast(numbers, 3) 20 | fmt.Printf("%v\n", skipLast3) // [1 2 3 4 5 6 7] 21 | } 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/ptrutils/nonNilSlice.md: -------------------------------------------------------------------------------- 1 | # NonNilSlice 2 | 3 | `func NonNilSlice[T any](ptrs []*T) []*T` 4 | 5 | NonNilSlice returns a new slice containing only non-nil pointers from the input. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | ptr "github.com/Goldziher/go-utils/ptrutils" 14 | ) 15 | 16 | func main() { 17 | ptrs := []*int{ptr.To(1), nil, ptr.To(3), nil, ptr.To(5)} 18 | 19 | // Filter out nil pointers 20 | nonNil := ptr.NonNilSlice(ptrs) 21 | fmt.Printf("Length: %d\n", len(nonNil)) // 3 22 | 23 | for _, p := range nonNil { 24 | fmt.Println(*p) // 1, 3, 5 25 | } 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/ptrutils/toPointerSlice.md: -------------------------------------------------------------------------------- 1 | # ToPointerSlice 2 | 3 | `func ToPointerSlice[T any](values []T) []*T` 4 | 5 | ToPointerSlice converts a slice of values to a slice of pointers. Each element in the result points to the corresponding element in the input. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | ptr "github.com/Goldziher/go-utils/ptrutils" 14 | ) 15 | 16 | func main() { 17 | values := []int{1, 2, 3, 4, 5} 18 | 19 | // Convert to pointer slice 20 | ptrs := ptr.ToPointerSlice(values) 21 | 22 | for _, p := range ptrs { 23 | fmt.Println(*p) // 1, 2, 3, 4, 5 24 | } 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/sliceutils/head.md: -------------------------------------------------------------------------------- 1 | # Head 2 | 3 | `func Head[T any](slice []T) *T` 4 | 5 | Head returns a pointer to the first element of the slice. Returns nil if the slice is empty. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | numbers := []int{1, 2, 3, 4, 5} 18 | 19 | first := sliceutils.Head(numbers) 20 | if first != nil { 21 | fmt.Printf("First: %d\n", *first) // First: 1 22 | } 23 | 24 | empty := []int{} 25 | noFirst := sliceutils.Head(empty) 26 | fmt.Printf("Empty slice head: %v\n", noFirst) // nil 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/excutils/must.md: -------------------------------------------------------------------------------- 1 | # Must 2 | 3 | `func Must(err error, messages ...string)` 4 | 5 | Must panics if the error is not nil. This is useful for initialization or setup code where errors should never occur. The optional messages are formatted and included in the panic message. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "os" 12 | 13 | exc "github.com/Goldziher/go-utils/excutils" 14 | ) 15 | 16 | func main() { 17 | // Panic if error occurs 18 | err := os.Chdir("/tmp") 19 | exc.Must(err, "failed to change directory") 20 | 21 | // Continue if no error 22 | println("Successfully changed directory") 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/sliceutils/initial.md: -------------------------------------------------------------------------------- 1 | # Initial 2 | 3 | `func Initial[T any](slice []T) []T` 4 | 5 | Initial returns all elements of the slice except the last. Returns an empty slice if the slice has 0 or 1 elements. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | numbers := []int{1, 2, 3, 4, 5} 18 | 19 | allButLast := sliceutils.Initial(numbers) 20 | fmt.Printf("%v\n", allButLast) // [1 2 3 4] 21 | 22 | single := []int{1} 23 | emptyInitial := sliceutils.Initial(single) 24 | fmt.Printf("%v\n", emptyInitial) // [] 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/sliceutils/last.md: -------------------------------------------------------------------------------- 1 | # Last 2 | 3 | `func Last[T any](slice []T) *T` 4 | 5 | Last returns a pointer to the last element of the slice. Returns nil if the slice is empty. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | numbers := []int{1, 2, 3, 4, 5} 18 | 19 | lastElem := sliceutils.Last(numbers) 20 | if lastElem != nil { 21 | fmt.Printf("Last: %d\n", *lastElem) // Last: 5 22 | } 23 | 24 | empty := []int{} 25 | noLast := sliceutils.Last(empty) 26 | fmt.Printf("Empty slice last: %v\n", noLast) // nil 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/ptrutils/coalesce.md: -------------------------------------------------------------------------------- 1 | # Coalesce 2 | 3 | `func Coalesce[T any](ptrs ...*T) *T` 4 | 5 | Coalesce returns the first non-nil pointer from the list. Returns nil if all pointers are nil. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | ptr "github.com/Goldziher/go-utils/ptrutils" 14 | ) 15 | 16 | func main() { 17 | var a, b *int 18 | c := ptr.To(42) 19 | d := ptr.To(99) 20 | 21 | // Get first non-nil pointer 22 | result := ptr.Coalesce(a, b, c, d) 23 | fmt.Println(*result) // 42 24 | 25 | // All nil 26 | result = ptr.Coalesce((*int)(nil), (*int)(nil)) 27 | fmt.Println(result) // 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/sliceutils/insert.md: -------------------------------------------------------------------------------- 1 | # Insert 2 | 3 | `func Insert[T any](slice []T, i int, value T) []T` 4 | 5 | Insert takes a slice of type T, an index and a value of type T, inserting the value at the given index and shifting any 6 | existing elements to the right. 7 | 8 | Deprecated: prefer `slices.Insert(slice, i, value)` from the standard library. 9 | 10 | ```go 11 | package main 12 | 13 | import ( 14 | "fmt" 15 | 16 | "github.com/Goldziher/go-utils/sliceutils" 17 | ) 18 | 19 | func main() { 20 | numerals := []int{0, 2} 21 | 22 | numerals = sliceutils.Insert(numerals, 1, 1) 23 | 24 | fmt.Print(numerals) // [0, 1, 2] 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/mathutils/inRange.md: -------------------------------------------------------------------------------- 1 | # InRange 2 | 3 | `func InRange[T cmp.Ordered](value, min, max T) bool` 4 | 5 | InRange checks if a value is within a range [min, max] (inclusive). 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/mathutils" 14 | ) 15 | 16 | func main() { 17 | fmt.Println(mathutils.InRange(5, 0, 10)) // true 18 | fmt.Println(mathutils.InRange(0, 0, 10)) // true (inclusive) 19 | fmt.Println(mathutils.InRange(10, 0, 10)) // true (inclusive) 20 | fmt.Println(mathutils.InRange(-1, 0, 10)) // false 21 | fmt.Println(mathutils.InRange(11, 0, 10)) // false 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/sliceutils/includes.md: -------------------------------------------------------------------------------- 1 | # Includes 2 | 3 | `func Includes[T comparable](slice []T, value T) bool` 4 | 5 | Includes receives a slice of type T and a value of type T, determining whether or not the value is included in the 6 | slice. 7 | 8 | Deprecated: prefer `slices.Contains(slice, value)` from the standard library. 9 | 10 | ```go 11 | package main 12 | 13 | import ( 14 | "fmt" 15 | 16 | "github.com/Goldziher/go-utils/sliceutils" 17 | ) 18 | 19 | func main() { 20 | friends := []string{"John", "Bob", "Mendy", "Suzy", "John"} 21 | 22 | result := sliceutils.Includes(friends, "John") 23 | 24 | fmt.Print(result) // true 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/stringutils/padLeft.md: -------------------------------------------------------------------------------- 1 | # PadLeft 2 | 3 | `func PadLeft(str string, padWith string, padTo int) string` 4 | 5 | PadLeft - Pad a string to a certain length with another string on the left side. 6 | If padding string is more than one char, it might be trucated to fit padTo size. 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "github.com/Goldziher/go-utils/stringutils" 14 | ) 15 | 16 | func main() { 17 | result := stringutils.PadLeft("Azer", "_", 7) // "___Azer" 18 | fmt.Print(result) // "___Azer" 19 | 20 | result = stringutils.PadLeft("Azer", "_-", 7) // "_-_Azer" 21 | fmt.Print(result) // "_-_Azer" 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/excutils/allErr.md: -------------------------------------------------------------------------------- 1 | # AllErr 2 | 3 | `func AllErr(errs ...error) error` 4 | 5 | AllErr returns an error containing all non-nil errors from the list. Returns nil if all errors are nil. Multiple errors are joined using errors.Join (Go 1.20+). 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | 14 | exc "github.com/Goldziher/go-utils/excutils" 15 | ) 16 | 17 | func main() { 18 | err1 := errors.New("first error") 19 | err2 := errors.New("second error") 20 | 21 | // Join all non-nil errors 22 | err := exc.AllErr(nil, err1, nil, err2) 23 | fmt.Println(err) 24 | // first error 25 | // second error 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/maputils/fromEntries.md: -------------------------------------------------------------------------------- 1 | # FromEntries 2 | 3 | `func FromEntries[K comparable, V any](entries [][2]any) map[K]V` 4 | 5 | FromEntries creates a map from a slice of key-value pairs. If duplicate keys exist, later values overwrite earlier ones. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/maputils" 14 | ) 15 | 16 | func main() { 17 | entries := [][2]any{ 18 | {"name", "Alice"}, 19 | {"age", 30}, 20 | {"role", "admin"}, 21 | } 22 | 23 | user := maputils.FromEntries[string, any](entries) 24 | 25 | fmt.Printf("%v\n", user) 26 | // map[age:30 name:Alice role:admin] 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/sliceutils/findIndexesOf.md: -------------------------------------------------------------------------------- 1 | # FindIndexesOf 2 | 3 | `func FindIndexes[T any](slice []T, predicate func(value T, index int, slice []T) bool) []int` 4 | 5 | FindIndexesOf takes a slice of type T and a value of type T, returning a slice containing all indexes where elements 6 | equal the passed in value. 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "strings" 14 | 15 | "github.com/Goldziher/go-utils/sliceutils" 16 | ) 17 | 18 | func main() { 19 | friends := []string{"John", "Bob", "Mendy", "Suzy", "John"} 20 | 21 | result := sliceutils.FindIndexesOf(friends, "John") 22 | 23 | fmt.Print(result) // [0, 4] 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/sliceutils/merge.md: -------------------------------------------------------------------------------- 1 | # Merge 2 | 3 | `func Merge[T any](slices ...[]T) (mergedSlice []T)` 4 | 5 | Merge takes slices of type T and merges them into a single slice of type T, preserving their order. 6 | 7 | Deprecated: prefer `slices.Concat(slices...)` from the standard library. 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | 15 | "github.com/Goldziher/go-utils/sliceutils" 16 | ) 17 | 18 | func main() { 19 | first := []int{1, 2, 3} 20 | second := []int{4, 5, 6} 21 | third := []int{7, 8, 9} 22 | 23 | result := sliceutils.Merge(first, second, third) 24 | 25 | fmt.Print(result) // [1, 2, 3, 4, 5, 6, 7, 8, 9] 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/stringutils/padRight.md: -------------------------------------------------------------------------------- 1 | # PadRight 2 | 3 | `func PadRight(str string, padWith string, padTo int) string` 4 | 5 | PadRight - Pad a string to a certain length with another string on the right side. 6 | If padding string is more than one char, it might be trucated to fit padTo size. 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "github.com/Goldziher/go-utils/stringutils" 14 | ) 15 | 16 | func main() { 17 | result := stringutils.PadRight("Azer", "_", 7) // "Azer___" 18 | fmt.Print(result) // "Azer___" 19 | 20 | 21 | result = stringutils.PadRight("Azer", "_-", 7) // "Azer_-_" 22 | fmt.Print(result) // "Azer_-_" 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/ptrutils/deref.md: -------------------------------------------------------------------------------- 1 | # Deref 2 | 3 | `func Deref[T any](ptr *T, def T) T` 4 | 5 | Deref dereferences ptr and returns the value it points to if not nil, or else returns def (the default value). This provides safe pointer dereferencing without nil checks. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | ptr "github.com/Goldziher/go-utils/ptrutils" 14 | ) 15 | 16 | func main() { 17 | numPtr := ptr.To(42) 18 | value := ptr.Deref(numPtr, 0) 19 | fmt.Println(value) // 42 20 | 21 | // Safe handling of nil pointers 22 | var nilPtr *int 23 | defaultValue := ptr.Deref(nilPtr, 99) 24 | fmt.Println(defaultValue) // 99 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/excutils/firstErr.md: -------------------------------------------------------------------------------- 1 | # FirstErr 2 | 3 | `func FirstErr(errs ...error) error` 4 | 5 | FirstErr returns the first non-nil error from a list of errors. Returns nil if all errors are nil. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | 14 | exc "github.com/Goldziher/go-utils/excutils" 15 | ) 16 | 17 | func main() { 18 | err1 := errors.New("first error") 19 | err2 := errors.New("second error") 20 | 21 | // Get first non-nil error 22 | err := exc.FirstErr(nil, nil, err1, err2) 23 | fmt.Println(err) // first error 24 | 25 | // All nil 26 | err = exc.FirstErr(nil, nil, nil) 27 | fmt.Println(err) // 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/maputils/forEach.md: -------------------------------------------------------------------------------- 1 | # ForEach 2 | 3 | `func ForEach[K comparable, V any](mapInstance map[K]V, function func(key K, value V))` 4 | 5 | ForEach given a map with keys K and values V, executes the passed in function for each key-value pair. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/maputils" 14 | ) 15 | 16 | func main() { 17 | vegetables := map[string]int{ 18 | "potatoes": 5, 19 | "carrots": 10, 20 | } 21 | 22 | maputils.ForEach(vegetables, func(key string, value int) { 23 | fmt.Printf("Buy %d Kg of %s", value, key) // "Buy 5 Kg of potatoes", "Buy 10 Kg of carrots" 24 | }) 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/maputils/has.md: -------------------------------------------------------------------------------- 1 | # Has 2 | 3 | `func Has[K comparable, V any](mapInstance map[K]V, key K) bool` 4 | 5 | Has checks if a key exists in the map. This is equivalent to the two-value form of map lookup but more expressive. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/maputils" 14 | ) 15 | 16 | func main() { 17 | config := map[string]string{ 18 | "host": "localhost", 19 | "port": "8080", 20 | } 21 | 22 | if maputils.Has(config, "host") { 23 | fmt.Println("Host is configured") 24 | } 25 | 26 | if !maputils.Has(config, "database") { 27 | fmt.Println("Database not configured") 28 | } 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/sliceutils/take.md: -------------------------------------------------------------------------------- 1 | # Take 2 | 3 | `func Take[T any](slice []T, n int) []T` 4 | 5 | Take returns the first n elements from the slice. If n is greater than the slice length, returns the entire slice. If n is zero or negative, returns an empty slice. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 18 | 19 | first3 := sliceutils.Take(numbers, 3) 20 | fmt.Printf("%v\n", first3) // [1 2 3] 21 | 22 | tooMany := sliceutils.Take(numbers, 100) 23 | fmt.Printf("%v\n", tooMany) // [1 2 3 4 5 6 7 8 9 10] 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/sliceutils/takeWhile.md: -------------------------------------------------------------------------------- 1 | # TakeWhile 2 | 3 | `func TakeWhile[T any](slice []T, predicate func(T) bool) []T` 4 | 5 | TakeWhile returns elements from the beginning of the slice while the predicate returns true. Stops at the first element where the predicate returns false. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 18 | 19 | // Take while less than 5 20 | result := sliceutils.TakeWhile(numbers, func(n int) bool { 21 | return n < 5 22 | }) 23 | 24 | fmt.Printf("%v\n", result) // [1 2 3 4] 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/sliceutils/findLastIndexOf.md: -------------------------------------------------------------------------------- 1 | # FindLastIndexOf 2 | 3 | `func FindLastIndexOf[T comparable](slice []T, value T) int` 4 | 5 | FindLastIndexOf takes a slice of type T and a value of type T. If any element in the slice equals the given value, the 6 | last index of is occurrence is returned. If no element is found, `-1` is returned. 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "strings" 14 | 15 | "github.com/Goldziher/go-utils/sliceutils" 16 | ) 17 | 18 | func main() { 19 | friends := []string{"John", "Bob", "Mendy", "Suzy", "John"} 20 | 21 | result := sliceutils.FindLastIndexOf(friends, "John") 22 | 23 | fmt.Print(result) // 4 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/sliceutils/skip.md: -------------------------------------------------------------------------------- 1 | # Skip 2 | 3 | `func Skip[T any](slice []T, n int) []T` 4 | 5 | Skip returns the slice with the first n elements removed. If n is greater than or equal to the slice length, returns an empty slice. If n is zero or negative, returns the original slice. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 18 | 19 | skip3 := sliceutils.Skip(numbers, 3) 20 | fmt.Printf("%v\n", skip3) // [4 5 6 7 8 9 10] 21 | 22 | skipAll := sliceutils.Skip(numbers, 100) 23 | fmt.Printf("%v\n", skipAll) // [] 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/excutils/mustResult.md: -------------------------------------------------------------------------------- 1 | # MustResult 2 | 3 | `func MustResult[T any](value T, err error, messages ...string) T` 4 | 5 | MustResult panics if the error is not nil, otherwise returns the result. This is useful for cases where an error should never occur and you want to fail fast. The optional messages are formatted and included in the panic message. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "os" 12 | 13 | exc "github.com/Goldziher/go-utils/excutils" 14 | ) 15 | 16 | func main() { 17 | // Panic if error, otherwise return file 18 | file := exc.MustResult(os.Open("config.json"), "failed to open config") 19 | defer file.Close() 20 | 21 | // Use file... 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/urlutils/queryStringifyMap.md: -------------------------------------------------------------------------------- 1 | # QueryStringifyMap 2 | 3 | `func QueryStringifyMap[K comparable, V any](values map[K]V) string` 4 | 5 | Creates a query string from a given map instance. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/urlutils" 14 | ) 15 | 16 | func main() { 17 | values := map[string]any{ 18 | "user": "moishe", 19 | "active": true, 20 | "age": 100, 21 | "friends": []int{1, 2, 3, 4, 5, 6}, 22 | } 23 | 24 | result := urlutils.QueryStringifyMap(values) 25 | 26 | fmt.Print(result) // "active=true&age=100&friends=1&friends=2&friends=3&friends=4&friends=5&friends=6&user=moishe" 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/sliceutils/forEach.md: -------------------------------------------------------------------------------- 1 | # ForEach 2 | 3 | `func ForEach[T any](slice []T, function func(value T, index int, slice []T))` 4 | 5 | ForEach executes the passed in function for each element in the given slice. The function is passed the current element, 6 | the current index and the slice as function arguments. 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | 14 | "github.com/Goldziher/go-utils/sliceutils" 15 | ) 16 | 17 | func main() { 18 | result := 0 19 | numerals := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 20 | 21 | sliceutils.ForEach(numerals, func(value int, index int, slice []int) { 22 | result += value 23 | }) 24 | 25 | fmt.Print(result) // 45 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/maputils/invert.md: -------------------------------------------------------------------------------- 1 | # Invert 2 | 3 | `func Invert[K, V comparable](mapInstance map[K]V) map[V]K` 4 | 5 | Invert swaps keys and values in a map. Both keys and values must be comparable types. If multiple keys have the same value, only one will remain (non-deterministic which one). 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/maputils" 14 | ) 15 | 16 | func main() { 17 | roles := map[string]int{ 18 | "admin": 1, 19 | "user": 2, 20 | "guest": 3, 21 | } 22 | 23 | // Invert to map from ID to role name 24 | byID := maputils.Invert(roles) 25 | 26 | fmt.Printf("%v\n", byID) 27 | // map[1:admin 2:user 3:guest] 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/maputils/toEntries.md: -------------------------------------------------------------------------------- 1 | # ToEntries 2 | 3 | `func ToEntries[K comparable, V any](mapInstance map[K]V) [][2]any` 4 | 5 | ToEntries converts a map to a slice of key-value pairs. Order is non-deterministic due to map iteration order. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/maputils" 14 | ) 15 | 16 | func main() { 17 | scores := map[string]int{ 18 | "Alice": 95, 19 | "Bob": 87, 20 | "Charlie": 92, 21 | } 22 | 23 | entries := maputils.ToEntries(scores) 24 | 25 | for _, entry := range entries { 26 | name := entry[0].(string) 27 | score := entry[1].(int) 28 | fmt.Printf("%s: %d\n", name, score) 29 | } 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/excutils/mustNotNil.md: -------------------------------------------------------------------------------- 1 | # MustNotNil 2 | 3 | `func MustNotNil[T any](value *T, messages ...string) T` 4 | 5 | MustNotNil panics with a formatted error message if the value is nil. Returns the dereferenced value if not nil. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | ptr "github.com/Goldziher/go-utils/ptrutils" 12 | exc "github.com/Goldziher/go-utils/excutils" 13 | ) 14 | 15 | func main() { 16 | numPtr := ptr.To(42) 17 | 18 | // Returns dereferenced value if not nil 19 | value := exc.MustNotNil(numPtr, "number must not be nil") 20 | println(value) // 42 21 | 22 | // Panics if nil 23 | var nilPtr *int 24 | exc.MustNotNil(nilPtr, "unexpected nil value") // panics 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/excutils/returnAnyErr.md: -------------------------------------------------------------------------------- 1 | # ReturnAnyErr 2 | 3 | `func ReturnAnyErr(values ...any) error` 4 | 5 | ReturnAnyErr returns the first error found in the list of values, if any. This is useful when dealing with variadic functions that may return errors. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | 14 | exc "github.com/Goldziher/go-utils/excutils" 15 | ) 16 | 17 | func main() { 18 | err1 := errors.New("an error") 19 | 20 | // Find any error in mixed values 21 | err := exc.ReturnAnyErr(42, "hello", err1, true) 22 | fmt.Println(err) // an error 23 | 24 | // No errors 25 | err = exc.ReturnAnyErr(42, "hello", true) 26 | fmt.Println(err) // 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/sliceutils/skipWhile.md: -------------------------------------------------------------------------------- 1 | # SkipWhile 2 | 3 | `func SkipWhile[T any](slice []T, predicate func(T) bool) []T` 4 | 5 | SkipWhile skips elements from the beginning of the slice while the predicate returns true. Returns the remaining elements starting from the first element where the predicate returns false. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 18 | 19 | // Skip while less than 5 20 | result := sliceutils.SkipWhile(numbers, func(n int) bool { 21 | return n < 5 22 | }) 23 | 24 | fmt.Printf("%v\n", result) // [5 6 7 8 9 10] 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/excutils/recoverWithValue.md: -------------------------------------------------------------------------------- 1 | # RecoverWithValue 2 | 3 | `func RecoverWithValue[T any](fn func() T, defaultValue T) (result T)` 4 | 5 | RecoverWithValue recovers from a panic and returns the specified default value. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | exc "github.com/Goldziher/go-utils/excutils" 14 | ) 15 | 16 | func main() { 17 | result := exc.RecoverWithValue(func() int { 18 | panic("something went wrong") 19 | return 42 20 | }, 0) 21 | 22 | fmt.Println(result) // 0 (default value returned due to panic) 23 | 24 | // No panic 25 | result = exc.RecoverWithValue(func() int { 26 | return 42 27 | }, 0) 28 | 29 | fmt.Println(result) // 42 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/mathutils/clamp.md: -------------------------------------------------------------------------------- 1 | # Clamp 2 | 3 | `func Clamp[T cmp.Ordered](value, min, max T) T` 4 | 5 | Clamp restricts a value to be within a specified range. If value < min, returns min. If value > max, returns max. Otherwise returns value. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/mathutils" 14 | ) 15 | 16 | func main() { 17 | // Clamp integers 18 | fmt.Println(mathutils.Clamp(5, 0, 10)) // 5 19 | fmt.Println(mathutils.Clamp(-5, 0, 10)) // 0 20 | fmt.Println(mathutils.Clamp(15, 0, 10)) // 10 21 | 22 | // Clamp floats 23 | fmt.Println(mathutils.Clamp(2.5, 0.0, 5.0)) // 2.5 24 | fmt.Println(mathutils.Clamp(6.0, 0.0, 5.0)) // 5.0 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/ptrutils/to.md: -------------------------------------------------------------------------------- 1 | # To 2 | 3 | `func To[T any](v T) *T` 4 | 5 | To returns a pointer to the given value. This is useful for creating pointers to literals or values in a single expression. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | ptr "github.com/Goldziher/go-utils/ptrutils" 14 | ) 15 | 16 | func main() { 17 | // Create pointers to literals 18 | numPtr := ptr.To(42) 19 | strPtr := ptr.To("hello") 20 | boolPtr := ptr.To(true) 21 | 22 | fmt.Printf("%v, %v, %v\n", *numPtr, *strPtr, *boolPtr) 23 | // 42, hello, true 24 | 25 | // Useful for struct fields that require pointers 26 | type Config struct { 27 | MaxRetries *int 28 | } 29 | config := Config{MaxRetries: ptr.To(5)} 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/sliceutils/filter.md: -------------------------------------------------------------------------------- 1 | # Filter 2 | 3 | `func Filter[T any](slice []T, predicate func(value T, index int, slice []T) bool) []T` 4 | 5 | Filter takes a slice of type `T` and filters it using the given predicate function. The predicate is passed the current 6 | element, the current index and the slice as function arguments. 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | 14 | "github.com/Goldziher/go-utils/sliceutils" 15 | ) 16 | 17 | func main() { 18 | numerals := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 19 | 20 | oddNumbers := sliceutils.Filter(numerals, func(value int, index int, slice []int) bool { 21 | return value%2 != 0 22 | }) 23 | 24 | fmt.Printf("%v", oddNumbers) // [1 3 5 7 9] 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/sliceutils/windows.md: -------------------------------------------------------------------------------- 1 | # Windows 2 | 3 | `func Windows[T any](slice []T, size int) [][]T` 4 | 5 | Windows returns a slice of sliding windows of the specified size. Each window is a slice of consecutive elements. If size is greater than the slice length or less than 1, returns an empty slice. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | numbers := []int{1, 2, 3, 4, 5} 18 | 19 | // Get all windows of size 3 20 | windows := sliceutils.Windows(numbers, 3) 21 | fmt.Printf("%v\n", windows) 22 | // [[1 2 3] [2 3 4] [3 4 5]] 23 | 24 | // Useful for calculating rolling averages, moving statistics, etc. 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/excutils/try.md: -------------------------------------------------------------------------------- 1 | # Try 2 | 3 | `func Try(fn func() error) (err error)` 4 | 5 | Try executes a function and returns its error. This is useful for defer statements or cleanup operations where you want to capture errors. Recovers from panics and converts them to errors. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | exc "github.com/Goldziher/go-utils/excutils" 14 | ) 15 | 16 | func main() { 17 | err := exc.Try(func() error { 18 | // Code that might panic or return error 19 | return riskyOperation() 20 | }) 21 | 22 | if err != nil { 23 | fmt.Printf("Caught error: %v\n", err) 24 | } 25 | } 26 | 27 | func riskyOperation() error { 28 | // ... may return error or panic 29 | return nil 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/maputils/omit.md: -------------------------------------------------------------------------------- 1 | # Omit 2 | 3 | `func Omit[K comparable, V any](mapInstance map[K]V, keys []K) map[K]V` 4 | 5 | Omit creates a new map excluding the specified keys. This is the opposite of Pick. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/maputils" 14 | ) 15 | 16 | func main() { 17 | user := map[string]any{ 18 | "name": "Alice", 19 | "age": 30, 20 | "email": "alice@example.com", 21 | "password": "secret", 22 | "role": "admin", 23 | } 24 | 25 | // Remove sensitive fields 26 | safe := maputils.Omit(user, []string{"password"}) 27 | 28 | fmt.Printf("%v\n", safe) 29 | // map[age:30 email:alice@example.com name:Alice role:admin] 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/sliceutils/findIndexOf.md: -------------------------------------------------------------------------------- 1 | # FindIndexOf 2 | 3 | `func FindIndexOf[T comparable](slice []T, value T) int` 4 | 5 | FindIndexOf takes a slice of type T and a value of type T. If any element in the slice equals the given value, its index 6 | is returned. If no element is found, `-1` is returned. 7 | 8 | Deprecated: prefer `slices.Index(slice, value)` from the standard library. 9 | 10 | ```go 11 | package main 12 | 13 | import ( 14 | "fmt" 15 | 16 | "github.com/Goldziher/go-utils/sliceutils" 17 | ) 18 | 19 | func main() { 20 | days := []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} 21 | 22 | result := sliceutils.FindIndexOf(days, "Wednesday") 23 | 24 | fmt.Print(result) // 3 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/excutils/retry.md: -------------------------------------------------------------------------------- 1 | # Retry 2 | 3 | `func Retry(fn func() error, maxAttempts int) error` 4 | 5 | Retry executes a function up to maxAttempts times, returning the first successful result. Returns the last error if all attempts fail. Returns an error if maxAttempts is less than 1. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | 14 | exc "github.com/Goldziher/go-utils/excutils" 15 | ) 16 | 17 | func main() { 18 | attempts := 0 19 | 20 | err := exc.Retry(func() error { 21 | attempts++ 22 | if attempts < 3 { 23 | return errors.New("temporary error") 24 | } 25 | return nil 26 | }, 5) 27 | 28 | if err == nil { 29 | fmt.Printf("Succeeded after %d attempts\n", attempts) 30 | } 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/ptrutils/equal.md: -------------------------------------------------------------------------------- 1 | # Equal 2 | 3 | `func Equal[T comparable](a, b *T) bool` 4 | 5 | Equal compares two pointers for equality. Returns true if both are nil or both point to equal values. Returns false if one is nil and the other is not. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | ptr "github.com/Goldziher/go-utils/ptrutils" 14 | ) 15 | 16 | func main() { 17 | a := ptr.To(5) 18 | b := ptr.To(5) 19 | c := ptr.To(10) 20 | var d *int 21 | 22 | fmt.Println(ptr.Equal(a, b)) // true (same values) 23 | fmt.Println(ptr.Equal(a, c)) // false (different values) 24 | fmt.Println(ptr.Equal(a, d)) // false (one nil, one not) 25 | fmt.Println(ptr.Equal((*int)(nil), (*int)(nil))) // true (both nil) 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/sliceutils/countBy.md: -------------------------------------------------------------------------------- 1 | # CountBy 2 | 3 | `func CountBy[T any, K comparable](slice []T, keySelector func(T) K) map[K]int` 4 | 5 | CountBy counts the occurrences of each key extracted using the keySelector function. Returns a map where keys are the extracted keys and values are the counts. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | words := []string{"apple", "banana", "apricot", "blueberry", "avocado"} 18 | 19 | // Count words by first letter 20 | counts := sliceutils.CountBy(words, func(word string) rune { 21 | return rune(word[0]) 22 | }) 23 | 24 | fmt.Printf("%v\n", counts) 25 | // map[97:3 98:2] (a=3, b=2) 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/maputils/pick.md: -------------------------------------------------------------------------------- 1 | # Pick 2 | 3 | `func Pick[K comparable, V any](mapInstance map[K]V, keys []K) map[K]V` 4 | 5 | Pick creates a new map containing only the specified keys. Keys that don't exist in the original map are ignored. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/maputils" 14 | ) 15 | 16 | func main() { 17 | user := map[string]any{ 18 | "name": "Alice", 19 | "age": 30, 20 | "email": "alice@example.com", 21 | "password": "secret", 22 | "role": "admin", 23 | } 24 | 25 | // Pick only public fields 26 | public := maputils.Pick(user, []string{"name", "age", "role"}) 27 | 28 | fmt.Printf("%v\n", public) 29 | // map[age:30 name:Alice role:admin] 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/excutils/returnNotNil.md: -------------------------------------------------------------------------------- 1 | # ReturnNotNil 2 | 3 | `func ReturnNotNil[T any](value *T, messages ...string) *T` 4 | 5 | ReturnNotNil panics if the value is nil, otherwise returns the value. This is useful for asserting that a pointer must not be nil. The optional messages are formatted and included in the panic message. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | ptr "github.com/Goldziher/go-utils/ptrutils" 12 | exc "github.com/Goldziher/go-utils/excutils" 13 | ) 14 | 15 | func main() { 16 | numPtr := ptr.To(42) 17 | 18 | // Returns value if not nil 19 | result := exc.ReturnNotNil(numPtr, "number must not be nil") 20 | 21 | // Panics if nil 22 | var nilPtr *int 23 | exc.ReturnNotNil(nilPtr, "unexpected nil value") // panics 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/sliceutils/map.md: -------------------------------------------------------------------------------- 1 | # Map 2 | 3 | `func Map[T any, R any](slice []T, mapper func(value T, index int, slice []T) R) (mapped []R)` 4 | 5 | Map allows transforming the values in a slice by executing the given mapper function for each element in the slice. The 6 | function is passed the current element, the current index and the slice itself as function arguments. 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | 14 | "github.com/Goldziher/go-utils/sliceutils" 15 | ) 16 | 17 | func main() { 18 | numerals := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 19 | 20 | result := sliceutils.Map(numerals, func(value int, index int, slice []int) int { 21 | return value * 2 22 | }) 23 | 24 | fmt.Print(result) // [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/sliceutils/some.md: -------------------------------------------------------------------------------- 1 | # Some 2 | 3 | `func Some[T any](slice []T, predicate func(value T, index int, slice []T) bool) bool` 4 | 5 | Some takes a slice of type T and a predicate function, returning true if the predicate returned true for **some** 6 | elements. The function is passed the current element, the current index and the slice itself as function arguments. 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | 14 | "github.com/Goldziher/go-utils/sliceutils" 15 | ) 16 | 17 | func main() { 18 | friends := []string{"John", "Bob", "Mendy", "Suzy", "John"} 19 | 20 | result := sliceutils.Some(friends, func(value string, index int, slice []string) bool { 21 | return value == "Mandy" 22 | }) 23 | 24 | fmt.Print(result) // true 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/dateutils/overlap.md: -------------------------------------------------------------------------------- 1 | # Overlap 2 | 3 | `Overlap(start1 time.Time, end1 time.Time, start2 time.Time, end2 time.Time) bool` 4 | 5 | Overlap - returns true if two date intervals overlap. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "github.com/Goldziher/go-utils/dateutils" 12 | "time" 13 | ) 14 | 15 | func main() { 16 | s1, _ := time.Parse("2006-01-02", "2022-12-28") 17 | e1, _ := time.Parse("2006-01-02", "2022-12-31") 18 | 19 | s2, _ := time.Parse("2006-01-02", "2022-12-30") 20 | e2, _ := time.Parse("2006-01-02", "2023-01-01") 21 | 22 | s3, _ := time.Parse("2006-01-02", "2023-01-02") 23 | e3, _ := time.Parse("2006-01-02", "2023-01-04") 24 | 25 | dateutils.Overlap(s1, e1, s2, e2) // true 26 | dateutils.Overlap(s1, e1, s3, e3) // false 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/maputils/get.md: -------------------------------------------------------------------------------- 1 | # Get 2 | 3 | `func Get[K comparable, V any](mapInstance map[K]V, key K, defaultValue V) V` 4 | 5 | Get safely gets a value from the map with a default fallback if the key doesn't exist. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/maputils" 14 | ) 15 | 16 | func main() { 17 | config := map[string]int{ 18 | "maxConnections": 100, 19 | "timeout": 30, 20 | } 21 | 22 | // Get existing key 23 | maxConn := maputils.Get(config, "maxConnections", 50) 24 | fmt.Printf("Max connections: %d\n", maxConn) // 100 25 | 26 | // Get missing key with default 27 | bufferSize := maputils.Get(config, "bufferSize", 1024) 28 | fmt.Printf("Buffer size: %d\n", bufferSize) // 1024 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/sliceutils/every.md: -------------------------------------------------------------------------------- 1 | # Every 2 | 3 | `func Every[T any](slice []T, predicate func(value T, index int, slice []T) bool) bool` 4 | 5 | Every takes a slice of type T and a predicate function, returning true if the predicate returned true for **every** 6 | elements. The function is passed the current element, the current index and the slice itself as function arguments. 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | 14 | "github.com/Goldziher/go-utils/sliceutils" 15 | ) 16 | 17 | func main() { 18 | friends := []string{"John", "Bob", "Mendy", "Suzy", "John"} 19 | 20 | result := sliceutils.Every(friends, func(value string, index int, slice []string) bool { 21 | return value == "Mandy" 22 | }) 23 | 24 | fmt.Print(result) // false 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/mathutils/isPrime.md: -------------------------------------------------------------------------------- 1 | # IsPrime 2 | 3 | `func IsPrime[T constraints.Integer](n T) bool` 4 | 5 | IsPrime checks if a number is prime. Returns false for numbers less than 2. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/mathutils" 14 | ) 15 | 16 | func main() { 17 | // Prime numbers 18 | fmt.Println(mathutils.IsPrime(2)) // true 19 | fmt.Println(mathutils.IsPrime(3)) // true 20 | fmt.Println(mathutils.IsPrime(17)) // true 21 | fmt.Println(mathutils.IsPrime(19)) // true 22 | 23 | // Non-prime numbers 24 | fmt.Println(mathutils.IsPrime(0)) // false 25 | fmt.Println(mathutils.IsPrime(1)) // false 26 | fmt.Println(mathutils.IsPrime(4)) // false 27 | fmt.Println(mathutils.IsPrime(15)) // false 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/maputils/filter.md: -------------------------------------------------------------------------------- 1 | # Filter 2 | 3 | `func Filter[K comparable, V any](mapInstance map[K]V, function func(key K, value V) bool) map[K]V` 4 | 5 | Filter takes a map with keys K and values V, and executes the passed in function for each key-value pair. If the filter 6 | function returns true, the key-value pair will be included in the output, otherwise it is filtered out. 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | 14 | "github.com/Goldziher/go-utils/maputils" 15 | ) 16 | 17 | func main() { 18 | vegetables := map[string]int{ 19 | "potatoes": 5, 20 | "carrots": 10, 21 | } 22 | 23 | result := maputils.Filter(vegetables, func(key string, value int) bool { 24 | return value == 5 25 | }) 26 | 27 | fmt.Print(result) // { "potatoes": 5 } 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/maputils/map.md: -------------------------------------------------------------------------------- 1 | # Map 2 | 3 | `func Map[K comparable, V any, R any](mapInstance map[K]V, mapper func(key K, value V) R) map[K]R` 4 | 5 | Map transforms map values using a mapper function, returning a new map with transformed values while preserving keys. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "strings" 13 | 14 | "github.com/Goldziher/go-utils/maputils" 15 | ) 16 | 17 | func main() { 18 | prices := map[string]int{ 19 | "apple": 100, 20 | "banana": 75, 21 | "cherry": 200, 22 | } 23 | 24 | // Transform values to strings 25 | priceStrings := maputils.Map(prices, func(k string, v int) string { 26 | return fmt.Sprintf("$%d.%02d", v/100, v%100) 27 | }) 28 | 29 | fmt.Printf("%v\n", priceStrings) 30 | // map[apple:$1.00 banana:$0.75 cherry:$2.00] 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/sliceutils/compact.md: -------------------------------------------------------------------------------- 1 | # Compact 2 | 3 | `func Compact[T comparable](slice []T) []T` 4 | 5 | Compact removes all zero values from the slice. A zero value is determined by the zero value of type T (0 for numbers, "" for strings, nil for pointers, false for bools, etc.). 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | // Remove zero numbers 18 | numbers := []int{1, 0, 2, 0, 3, 0, 4, 5} 19 | compacted := sliceutils.Compact(numbers) 20 | fmt.Printf("%v\n", compacted) // [1 2 3 4 5] 21 | 22 | // Remove empty strings 23 | strings := []string{"hello", "", "world", "", "!"} 24 | compactedStrings := sliceutils.Compact(strings) 25 | fmt.Printf("%v\n", compactedStrings) // [hello world !] 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/sliceutils/flatMap.md: -------------------------------------------------------------------------------- 1 | # FlatMap 2 | 3 | `func FlatMap[T any, R any](slice []T, mapper func(value T, index int, slice []T) []R) []R` 4 | 5 | FlatMap receives a slice of type T, executes the passed in slice-mapper function for each element in the slice, and 6 | returns a flattened slice containing all the elements from all the mapped slices. 7 | The function is passed the current element, the current index and the slice itself as function arguments. 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | items := []int{1, 2, 3, 4} 18 | 19 | flatMapped := sliceutils.FlatMap(items, func(value int, index int, slice []int) []int { 20 | return []int{value, value * 2} 21 | }) // []int{1, 2, 2, 4, 3, 6, 4, 8} 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/sliceutils/partition.md: -------------------------------------------------------------------------------- 1 | # Partition 2 | 3 | `func Partition[T any](slice []T, predicate func(T) bool) (truthy []T, falsy []T)` 4 | 5 | Partition splits a slice into two slices based on a predicate function. The first slice contains elements for which the predicate returns true, the second contains elements for which it returns false. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 18 | 19 | // Partition into evens and odds 20 | evens, odds := sliceutils.Partition(numbers, func(n int) bool { 21 | return n%2 == 0 22 | }) 23 | 24 | fmt.Printf("Evens: %v\n", evens) // [2 4 6 8 10] 25 | fmt.Printf("Odds: %v\n", odds) // [1 3 5 7 9] 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/excutils/retryWithResult.md: -------------------------------------------------------------------------------- 1 | # RetryWithResult 2 | 3 | `func RetryWithResult[T any](fn func() (T, error), maxAttempts int) (T, error)` 4 | 5 | RetryWithResult executes a function up to maxAttempts times, returning the first successful result. Returns the result and error from the last attempt if all fail. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | 14 | exc "github.com/Goldziher/go-utils/excutils" 15 | ) 16 | 17 | func main() { 18 | attempts := 0 19 | 20 | result, err := exc.RetryWithResult(func() (int, error) { 21 | attempts++ 22 | if attempts < 3 { 23 | return 0, errors.New("temporary error") 24 | } 25 | return 42, nil 26 | }, 5) 27 | 28 | if err == nil { 29 | fmt.Printf("Got result: %d after %d attempts\n", result, attempts) 30 | } 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/maputils/copy.md: -------------------------------------------------------------------------------- 1 | # Copy 2 | 3 | `func Copy[K comparable, V any](mapInstance map[K]V) map[K]V` 4 | 5 | Copy takes a map with keys K and values V and returns a copy. 6 | 7 | Deprecated: prefer `maps.Clone(mapInstance)` from the standard library. 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "maps" 15 | 16 | "github.com/Goldziher/go-utils/maputils" 17 | ) 18 | 19 | func main() { 20 | vegetables := map[string]int{ 21 | "potatoes": 5, 22 | "carrots": 10, 23 | } 24 | 25 | copiedVegetables := maputils.Copy(vegetables) 26 | clonedVegetables := maps.Clone(vegetables) 27 | 28 | copiedVegetables["potatoes"] = 3 29 | 30 | fmt.Print(vegetables["potatoes"]) // 5 31 | fmt.Print(copiedVegetables["potatoes"]) // 3 32 | fmt.Print(clonedVegetables["potatoes"]) // 5 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/maputils/merge.md: -------------------------------------------------------------------------------- 1 | # Merge 2 | 3 | `func Merge[K comparable, V any](mapInstances ...map[K]V) map[K]V` 4 | 5 | Merge takes a variadic numbers of maps with keys K and values V and returns a merged map. Merging is done from left to 6 | right. If a key already exists in a previous map, its value is over-written. 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | 14 | "github.com/Goldziher/go-utils/maputils" 15 | ) 16 | 17 | func main() { 18 | vegetables := map[string]int{ 19 | "potatoes": 5, 20 | "carrots": 10, 21 | "tomatoes": 3, 22 | } 23 | 24 | fruits := map[string]int{ 25 | "bananas": 3, 26 | "tomatoes": 5, 27 | } 28 | 29 | result := maputils.Merge(vegetables, fruits) 30 | 31 | fmt.Print(result) //{ "potatoes": 5, "carrots": 10, "tomatoes": 5, "bananas": 3 } 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/maputils/mapKeys.md: -------------------------------------------------------------------------------- 1 | # MapKeys 2 | 3 | `func MapKeys[K comparable, V any, R comparable](mapInstance map[K]V, mapper func(key K, value V) R) map[R]V` 4 | 5 | MapKeys transforms map keys using a mapper function, returning a new map with transformed keys. If the mapper produces duplicate keys, later values will overwrite earlier ones. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "strings" 13 | 14 | "github.com/Goldziher/go-utils/maputils" 15 | ) 16 | 17 | func main() { 18 | data := map[string]int{ 19 | "apple": 5, 20 | "BANANA": 3, 21 | "Cherry": 8, 22 | } 23 | 24 | // Normalize keys to lowercase 25 | normalized := maputils.MapKeys(data, func(k string, v int) string { 26 | return strings.ToLower(k) 27 | }) 28 | 29 | fmt.Printf("%v\n", normalized) 30 | // map[apple:5 banana:3 cherry:8] 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/sliceutils/pluck.md: -------------------------------------------------------------------------------- 1 | # Pluck 2 | 3 | `func Pluck[I any, O any](input []I, getter func(I) *O) []O ` 4 | 5 | Pluck receives a slice of type I and a getter func to a field and returns an array containing requested field from each slice's item. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | items := []Pluckable{ 18 | { 19 | Code: "azer", 20 | Value: "Azer", 21 | }, 22 | { 23 | Code: "tyuio", 24 | Value: "Tyuio", 25 | }, 26 | } 27 | 28 | result1 := sliceutils.Pluck(items, func(item Pluckable) *string { 29 | return &item.Code 30 | }) // []string{"azer", "tyuio""} 31 | result2 := sliceutils.Pluck(items, func(item Pluckable) *string { 32 | return &item.Value 33 | }) // []string{"Azer", "Tyuio"} 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/dateutils/afterorequal.md: -------------------------------------------------------------------------------- 1 | # AfterOrEqual 2 | 3 | `func AfterOrEqual(milestone time.Time, date time.Time) bool` 4 | 5 | AfterOrEqual returns true if a date is equal to or after another date. 6 | 7 | Deprecated: prefer `!date.Before(milestone)` from the standard library. 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "time" 14 | 15 | "github.com/Goldziher/go-utils/dateutils" 16 | ) 17 | 18 | func main() { 19 | milestone, _ := time.Parse("2006-01-02", "2023-01-01") 20 | 21 | dBefore, _ := time.Parse("2006-01-02", "2022-12-31") 22 | dEqual, _ := time.Parse("2006-01-02", "2023-01-01") 23 | dAfter, _ := time.Parse("2006-01-02", "2023-01-31") 24 | 25 | dateutils.AfterOrEqual(milestone, dBefore) // false 26 | dateutils.AfterOrEqual(milestone, dEqual) // true 27 | dateutils.AfterOrEqual(milestone, dAfter) // true 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/dateutils/beforeorequal.md: -------------------------------------------------------------------------------- 1 | # BeforeOrEqual 2 | 3 | `func BeforeOrEqual(milestone time.Time, date time.Time) bool` 4 | 5 | BeforeOrEqual returns true if a date is before or equal to another date. 6 | 7 | Deprecated: prefer `!date.After(milestone)` from the standard library. 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "time" 14 | 15 | "github.com/Goldziher/go-utils/dateutils" 16 | ) 17 | 18 | func main() { 19 | milestone, _ := time.Parse("2006-01-02", "2023-01-01") 20 | 21 | dBefore, _ := time.Parse("2006-01-02", "2022-12-31") 22 | dEqual, _ := time.Parse("2006-01-02", "2023-01-01") 23 | dAfter, _ := time.Parse("2006-01-02", "2023-01-31") 24 | 25 | dateutils.BeforeOrEqual(milestone, dBefore) // true 26 | dateutils.BeforeOrEqual(milestone, dEqual) // true 27 | dateutils.BeforeOrEqual(milestone, dAfter) // false 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/sliceutils/minBy.md: -------------------------------------------------------------------------------- 1 | # MinBy 2 | 3 | `func MinBy[T any, O cmp.Ordered](slice []T, selector func(T) O) *T` 4 | 5 | MinBy returns a pointer to the element with the minimum value as determined by the selector function. Returns nil if the slice is empty. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | type Product struct { 18 | Name string 19 | Price float64 20 | } 21 | 22 | products := []Product{ 23 | {"Apple", 1.50}, 24 | {"Banana", 0.75}, 25 | {"Cherry", 2.00}, 26 | } 27 | 28 | cheapest := sliceutils.MinBy(products, func(p Product) float64 { 29 | return p.Price 30 | }) 31 | 32 | if cheapest != nil { 33 | fmt.Printf("Cheapest: %s at $%.2f\n", cheapest.Name, cheapest.Price) 34 | // Cheapest: Banana at $0.75 35 | } 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/sliceutils/maxBy.md: -------------------------------------------------------------------------------- 1 | # MaxBy 2 | 3 | `func MaxBy[T any, O cmp.Ordered](slice []T, selector func(T) O) *T` 4 | 5 | MaxBy returns a pointer to the element with the maximum value as determined by the selector function. Returns nil if the slice is empty. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | type Product struct { 18 | Name string 19 | Price float64 20 | } 21 | 22 | products := []Product{ 23 | {"Apple", 1.50}, 24 | {"Banana", 0.75}, 25 | {"Cherry", 2.00}, 26 | } 27 | 28 | expensive := sliceutils.MaxBy(products, func(p Product) float64 { 29 | return p.Price 30 | }) 31 | 32 | if expensive != nil { 33 | fmt.Printf("Most expensive: %s at $%.2f\n", expensive.Name, expensive.Price) 34 | // Most expensive: Cherry at $2.00 35 | } 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/sliceutils/reduce.md: -------------------------------------------------------------------------------- 1 | # Reduce 2 | 3 | `func Reduce[T any, R any](slice []T, reducer func(acc R, value T, index int, slice []T) R, initial R) R` 4 | 5 | Reduce allows transforming the slice and its values into a different value by executing the given reducer function for 6 | each element in the slice. The function is passed the accumulator, current element, current index and the slice as 7 | function arguments. The third argument to reduce is the initial value. 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | 15 | "github.com/Goldziher/go-utils/sliceutils" 16 | ) 17 | 18 | func main() { 19 | numerals := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 20 | 21 | sum := sliceutils.Reduce( 22 | numerals, 23 | func(acc int, cur int, index int, slice []int) int { 24 | return acc + cur 25 | }, 26 | 0, 27 | ) 28 | 29 | fmt.Print(sum) // 45 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/sliceutils/distinctBy.md: -------------------------------------------------------------------------------- 1 | # DistinctBy 2 | 3 | `func DistinctBy[T any, K comparable](slice []T, keySelector func(T) K) []T` 4 | 5 | DistinctBy returns a slice containing only the first occurrence of each element, where uniqueness is determined by the key returned from the keySelector function. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | type User struct { 18 | Name string 19 | Age int 20 | } 21 | 22 | users := []User{ 23 | {"Alice", 30}, 24 | {"Bob", 25}, 25 | {"Charlie", 30}, 26 | {"David", 25}, 27 | } 28 | 29 | // Get users with distinct ages (first occurrence only) 30 | distinctByAge := sliceutils.DistinctBy(users, func(u User) int { 31 | return u.Age 32 | }) 33 | 34 | fmt.Printf("%v\n", distinctByAge) 35 | // [{Alice 30} {Bob 25}] 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/sliceutils/findIndexes.md: -------------------------------------------------------------------------------- 1 | # FindIndexes 2 | 3 | `func FindIndexes[T any](slice []T, predicate func(value T, index int, slice []T) bool) []int` 4 | 5 | FindIndexes takes a slice of type T and executes the passed in predicate function for each element in the slice, 6 | returning a slice containing all indexes for which the predicate returned true. The function is passed the current 7 | element, the current index and the slice itself as function arguments. 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "strings" 15 | 16 | "github.com/Goldziher/go-utils/sliceutils" 17 | ) 18 | 19 | func main() { 20 | friends := []string{"John", "Bob", "Mendy", "Suzy", "John"} 21 | 22 | result := sliceutils.FindIndexes(friends, func(value string, index int, slice []string) bool { 23 | return value == "John" 24 | }) 25 | 26 | fmt.Print(result) // [0, 4] 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/sliceutils/find.md: -------------------------------------------------------------------------------- 1 | # Find 2 | 3 | `func Find[T any](slice []T, predicate func(value T, index int, slice []T) bool) *T` 4 | 5 | Find takes a slice of type T and executes the passed in predicate function for each element in the slice. If the 6 | predicate returns true, a pointer to the element is returned. If no element is found, nil is returned. The function is 7 | passed the current element, the current index and the slice itself as function arguments. 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "strings" 15 | 16 | "github.com/Goldziher/go-utils/sliceutils" 17 | ) 18 | 19 | func main() { 20 | days := []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} 21 | 22 | result := sliceutils.Find(days, func(value string, index int, slice []string) bool { 23 | return strings.Contains(value, "Wed") 24 | }) 25 | 26 | fmt.Print(result) // "Wednesday" 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/sliceutils/findLastIndex.md: -------------------------------------------------------------------------------- 1 | # FindLastIndex 2 | 3 | `func FindLastIndex[T any](slice []T, predicate func(value T, index int, slice []T) bool) int` 4 | 5 | FindLastIndex takes a slice of type T and executes the passed in predicate function for each element in the slice 6 | starting from its end. If the predicate returns true, the element's index is returned. If no element is found, `-1` is 7 | returned. The function is passed the current element, the current index and the slice itself as function arguments. 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "strings" 15 | 16 | "github.com/Goldziher/go-utils/sliceutils" 17 | ) 18 | 19 | func main() { 20 | friends := []string{"John", "Bob", "Mendy", "Suzy", "John"} 21 | 22 | result := sliceutils.FindLastIndex(friends, func(value string, index int, slice []string) bool { 23 | return value == "John" 24 | }) 25 | 26 | fmt.Print(result) // 4 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/sliceutils/findIndex.md: -------------------------------------------------------------------------------- 1 | # FindIndex 2 | 3 | `func FindIndex[T any](slice []T, predicate func(value T, index int, slice []T) bool) int` 4 | 5 | FindIndex takes a slice of type T and executes the passed in predicate function for each element in the slice. If the 6 | predicate returns true, the element's index is returned. If no element is found, `-1` is returned. The function is 7 | passed the current element, the current index and the slice itself as function arguments. 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "strings" 15 | 16 | "github.com/Goldziher/go-utils/sliceutils" 17 | ) 18 | 19 | func main() { 20 | days := []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} 21 | 22 | result := sliceutils.FindIndex(days, func(value string, index int, slice []string) bool { 23 | return strings.Contains(value, "Wed") 24 | }) 25 | 26 | fmt.Print(result) // 3 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_stages: [commit] 2 | repos: 3 | - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook 4 | rev: v9.23.0 5 | hooks: 6 | - id: commitlint 7 | stages: [commit-msg] 8 | additional_dependencies: ["@commitlint/config-conventional"] 9 | - repo: https://github.com/pre-commit/pre-commit-hooks 10 | rev: v6.0.0 11 | hooks: 12 | - id: trailing-whitespace 13 | - id: end-of-file-fixer 14 | - id: check-yaml 15 | - id: check-added-large-files 16 | - repo: https://github.com/Goldziher/ai-rulez 17 | rev: v2.4.1 18 | hooks: 19 | - id: ai-rulez-validate 20 | - id: ai-rulez-generate 21 | - repo: https://github.com/golangci/golangci-lint 22 | rev: v2.5.0 23 | hooks: 24 | - id: golangci-lint 25 | args: ["--timeout", "3m0s"] 26 | - repo: https://github.com/segmentio/golines 27 | rev: v0.13.0 28 | hooks: 29 | - id: golines 30 | -------------------------------------------------------------------------------- /docs/sliceutils/groupBy.md: -------------------------------------------------------------------------------- 1 | # GroupBy 2 | 3 | `func GroupBy[T any, K comparable](slice []T, keySelector func(T) K) map[K][]T` 4 | 5 | GroupBy groups elements of a slice by a key extracted using the keySelector function. Returns a map where keys are the grouping keys and values are slices of elements that share that key. This is a LINQ-style operation useful for categorizing or organizing data. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/sliceutils" 14 | ) 15 | 16 | func main() { 17 | type User struct { 18 | Name string 19 | Age int 20 | } 21 | 22 | users := []User{ 23 | {"Alice", 30}, 24 | {"Bob", 25}, 25 | {"Charlie", 30}, 26 | {"David", 25}, 27 | } 28 | 29 | // Group users by age 30 | byAge := sliceutils.GroupBy(users, func(u User) int { 31 | return u.Age 32 | }) 33 | 34 | fmt.Printf("%v\n", byAge) 35 | // map[25:[{Bob 25} {David 25}] 30:[{Alice 30} {Charlie 30}]] 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/structutils/forEach.md: -------------------------------------------------------------------------------- 1 | # ForEach 2 | 3 | `func ForEach[T any](structInstance T, function func(key string, value any, tag reflect.StructTag))` 4 | 5 | Takes a struct and calls the passed in function for each of its **visible** fields, passing to in the field's name, 6 | value and tag. 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "reflect" 14 | 15 | "github.com/Goldziher/go-utils/structutils" 16 | ) 17 | 18 | type Person struct { 19 | FirstName string 20 | LastName string 21 | Age int `myTag:"myValue"` 22 | } 23 | 24 | func main() { 25 | personInstance := Person{ 26 | FirstName: "Moishe", 27 | LastName: "Zuchmir", 28 | Age: 100, 29 | } 30 | 31 | structutils.ForEach(personInstance, func(key string, value any, tag reflect.StructTag) { 32 | fmt.Printf("%v - %v - %v\n", key, value, tag.Get("myTag")) 33 | }) 34 | 35 | // FirstName - Moishe 36 | // LastName - Zuchmir 37 | // Age - 100 - myValue 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/mathutils/index.md: -------------------------------------------------------------------------------- 1 | # mathutils 2 | 3 | Generic math operations with type constraints. 4 | 5 | ## Overview 6 | 7 | The `mathutils` package provides mathematical utilities that work with generic numeric types through Go generics and constraints. All functions use appropriate type constraints (`cmp.Ordered`, `constraints.Integer`, etc.) for compile-time type safety. 8 | 9 | ## Functions 10 | 11 | **Range Operations**: Clamp, InRange 12 | **Numeric Operations**: Abs, Average 13 | **Number Theory**: Gcd, Lcm, IsPrime 14 | 15 | ## Example 16 | 17 | ```go 18 | import "github.com/Goldziher/go-utils/mathutils" 19 | 20 | // Clamp values to a range 21 | clamped := mathutils.Clamp(150, 0, 100) // 100 22 | 23 | // Check if value is in range 24 | inRange := mathutils.InRange(50, 0, 100) // true 25 | 26 | // Number theory 27 | gcd := mathutils.Gcd(48, 18) // 6 28 | lcm := mathutils.Lcm(4, 6) // 12 29 | prime := mathutils.IsPrime(17) // true 30 | 31 | // Statistics 32 | values := []int{1, 2, 3, 4, 5} 33 | avg := mathutils.Average(values) // 3.0 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/ptrutils/index.md: -------------------------------------------------------------------------------- 1 | # ptrutils 2 | 3 | Pointer utilities for safe pointer operations and conversions. 4 | 5 | ## Overview 6 | 7 | The `ptrutils` package (imported as `ptr`) provides utilities for working with pointers, including creating pointers to literals, safe dereferencing, and pointer slice operations. 8 | 9 | ## Functions 10 | 11 | **Creation**: To 12 | **Dereferencing**: Deref, DerefSlice 13 | **Comparison**: Equal, IsNil 14 | **Utilities**: Coalesce, ToSlice, NonNilSlice, ToPointerSlice 15 | 16 | ## Example 17 | 18 | ```go 19 | import ptr "github.com/Goldziher/go-utils/ptrutils" 20 | 21 | // Create pointers to literals 22 | numPtr := ptr.To(42) 23 | strPtr := ptr.To("hello") 24 | 25 | // Safe dereferencing with default 26 | value := ptr.Deref(numPtr, 0) // 42 27 | nilValue := ptr.Deref((*int)(nil), 99) // 99 28 | 29 | // Coalesce - get first non-nil 30 | first := ptr.Coalesce(nil, nil, ptr.To(5), ptr.To(10)) // *5 31 | 32 | // Convert slice to pointer slice 33 | values := []int{1, 2, 3} 34 | ptrs := ptr.ToPointerSlice(values) // []*int 35 | ``` 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | To contribute code changes or update the documentation, please follow these steps: 4 | 5 | 1. Fork the upstream repository and clone the fork locally. 6 | 2. Install [prek](https://github.com/RobertCraigie/prek) (a Rust rewrite of pre-commit): 7 | 8 | ```shell 9 | # Using uv (recommended) 10 | uv tool install prek 11 | 12 | # Or using pip 13 | pip install prek 14 | ``` 15 | 16 | 3. Navigate to the cloned repository's directory and install the hooks by running: 17 | 18 | ```shell 19 | prek install && prek install --hook-type commit-msg 20 | ``` 21 | 22 | 4. Make whatever changes and additions you wish and commit these - please try to keep your commit history clean. 23 | 5. Create a pull request to the main repository with an explanation of your changes. 24 | 25 | Note: 26 | 27 | - if you add new code or modify existing code - 100% test coverage is mandatory and tests should be well written. 28 | - we follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) and this is enforced using commitlint via a pre-commit hook. 29 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | To contribute code changes or update the documentation, please follow these steps: 4 | 5 | 1. Fork the upstream repository and clone the fork locally. 6 | 2. Install [prek](https://github.com/RobertCraigie/prek) (a Rust rewrite of pre-commit): 7 | 8 | ```shell 9 | # Using uv (recommended) 10 | uv tool install prek 11 | 12 | # Or using pip 13 | pip install prek 14 | ``` 15 | 16 | 3. Navigate to the cloned repository's directory and install the hooks by running: 17 | 18 | ```shell 19 | prek install && prek install --hook-type commit-msg 20 | ``` 21 | 22 | 4. Make whatever changes and additions you wish and commit these - please try to keep your commit history clean. 23 | 5. Create a pull request to the main repository with an explanation of your changes. 24 | 25 | Note: 26 | 27 | - if you add new code or modify existing code - 100% test coverage is mandatory and tests should be well written. 28 | - we follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) and this is enforced using commitlint via a pre-commit hook. 29 | -------------------------------------------------------------------------------- /docs/maputils/groupBy.md: -------------------------------------------------------------------------------- 1 | # GroupBy 2 | 3 | `func GroupBy[K comparable, V any, G comparable](mapInstance map[K]V, grouper func(key K, value V) G) map[G]map[K]V` 4 | 5 | GroupBy groups map entries by a grouping key generated from each entry. Returns a map where keys are group identifiers and values are maps of entries belonging to that group. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "strings" 13 | 14 | "github.com/Goldziher/go-utils/maputils" 15 | ) 16 | 17 | func main() { 18 | scores := map[string]int{ 19 | "Alice": 95, 20 | "Bob": 67, 21 | "Charlie": 88, 22 | "David": 72, 23 | "Eve": 91, 24 | } 25 | 26 | // Group by grade (A: 90+, B: 80-89, C: 70-79, etc.) 27 | byGrade := maputils.GroupBy(scores, func(name string, score int) string { 28 | switch { 29 | case score >= 90: 30 | return "A" 31 | case score >= 80: 32 | return "B" 33 | case score >= 70: 34 | return "C" 35 | default: 36 | return "F" 37 | } 38 | }) 39 | 40 | fmt.Printf("%v\n", byGrade) 41 | // map[A:map[Alice:95 Eve:91] B:map[Charlie:88] C:map[Bob:67 David:72]] 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Na'aman Hirschfeld 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/excutils/index.md: -------------------------------------------------------------------------------- 1 | # excutils 2 | 3 | Exception-style error handling utilities for Go. 4 | 5 | ## Overview 6 | 7 | The `excutils` package (imported as `exc`) provides utilities for common error handling patterns including panic-on-error, error recovery, and safe error checking. While Go favors explicit error returns, these utilities can make certain patterns more concise. 8 | 9 | ## Functions 10 | 11 | **Panic on Error**: Must, MustResult, MustNotNil, ReturnNotNil 12 | **Recovery**: Try, Catch, RecoverWithValue 13 | **Error Utilities**: FirstErr, AllErr, ReturnAnyErr, IgnoreErr 14 | **Retry**: Retry, RetryWithResult 15 | 16 | ## Example 17 | 18 | ```go 19 | import exc "github.com/Goldziher/go-utils/excutils" 20 | 21 | // Panic on error (for setup/initialization) 22 | file := exc.MustResult(os.Open("config.json"), "failed to open config") 23 | 24 | // Safe recovery 25 | err := exc.Try(func() error { 26 | // Code that might panic or error 27 | return riskyOperation() 28 | }) 29 | 30 | // Get first error 31 | err := exc.FirstErr(err1, err2, err3) 32 | 33 | // Retry with exponential backoff 34 | err = exc.Retry(func() error { 35 | return httpClient.Do(req) 36 | }, 3) 37 | ``` 38 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 7 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= 9 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 14 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /docs/maputils/drop.md: -------------------------------------------------------------------------------- 1 | # Drop 2 | 3 | `func Drop[K comparable, V any](mapInstance map[K]V, keys []K) map[K]V` 4 | 5 | Drop takes a map with keys K and values V, and a slice of keys K, dropping all the key-value pairs that match the keys in the slice. 6 | 7 | Note: this function will modify the passed in map. To get a different object, use the Copy function to pass a copy to this function. 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | 15 | "github.com/Goldziher/go-utils/maputils" 16 | ) 17 | 18 | func main() { 19 | vegetables := map[string]int{ 20 | "potatoes": 5, 21 | "carrots": 10, 22 | "tomatoes": 3, 23 | } 24 | 25 | // Drop will modify the original map 26 | result := maputils.Drop(vegetables, []string{"carrots", "tomatoes"}) 27 | 28 | fmt.Print(result) // { "potatoes": 5 } 29 | fmt.Print(vegetables) // { "potatoes": 5 } - original map was modified 30 | 31 | // To avoid modifying the original, use Copy first 32 | vegetables2 := map[string]int{ 33 | "potatoes": 5, 34 | "carrots": 10, 35 | "tomatoes": 3, 36 | } 37 | 38 | copied := maputils.Copy(vegetables2) 39 | result2 := maputils.Drop(copied, []string{"carrots"}) 40 | 41 | fmt.Print(result2) // { "potatoes": 5, "tomatoes": 3 } 42 | fmt.Print(vegetables2) // { "potatoes": 5, "carrots": 10, "tomatoes": 3 } - original unchanged 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.9.1] - 2025-02-13 8 | ### Added 9 | - Restored deprecated helpers removed in 1.9.0 (`sliceutils.FindIndexOf`, `Includes`, `Merge`, `Insert`, `Copy`; `dateutils.BeforeOrEqual`, `AfterOrEqual`) with guidance to stdlib replacements. 10 | - Reinstated documentation pages and mkdocs navigation for the deprecated helpers; added maputils Copy doc note for `maps.Clone`. 11 | 12 | ### Fixed 13 | - Refined `stringutils.Stringify` for typed nil pointers and clearer flow; resolved lint issues. 14 | - Corrected typo in `dateutils.Ceil` comment (“Subtract”). 15 | 16 | ### Notes 17 | - Release tagged as `v1.9.1` and published. 18 | 19 | ## [1.9.0] - 2025-02-XX 20 | ### Removed 21 | - Removed several helpers now provided by the Go standard library (`sliceutils.FindIndexOf`, `Includes`, `Merge`, `Insert`, `Copy`; `dateutils.BeforeOrEqual`, `AfterOrEqual`). 22 | 23 | ### Added 24 | - New utilities across sliceutils, maputils, dateutils, ptrutils, mathutils, excutils, urlutils, structutils (see docs for details). 25 | 26 | [1.9.1]: https://github.com/Goldziher/go-utils/releases/tag/v1.9.1 27 | [1.9.0]: https://github.com/Goldziher/go-utils/releases/tag/v1.9.0 28 | -------------------------------------------------------------------------------- /docs/maputils/index.md: -------------------------------------------------------------------------------- 1 | # maputils 2 | 3 | Utilities for map transformations and operations. 4 | 5 | ## Overview 6 | 7 | The `maputils` package provides type-safe generic functions for working with maps. All functions leverage Go generics to work with any map type `map[K]V` where `K` is `comparable`. 8 | 9 | ## Functions 10 | 11 | **Extraction**: Keys, Values 12 | **Transformation**: Filter, Map 13 | **Iteration**: ForEach 14 | **Combination**: Merge 15 | **Manipulation**: Drop, Invert, Pick, Omit 16 | 17 | ## Example 18 | 19 | ```go 20 | import "github.com/Goldziher/go-utils/maputils" 21 | 22 | data := map[string]int{ 23 | "apple": 5, 24 | "banana": 3, 25 | "cherry": 8, 26 | } 27 | 28 | // Extract keys and values 29 | keys := maputils.Keys(data) // []string{"apple", "banana", "cherry"} 30 | values := maputils.Values(data) // []int{5, 3, 8} 31 | 32 | // Filter entries 33 | filtered := maputils.Filter(data, func(k string, v int) bool { 34 | return v > 4 35 | }) 36 | // Result: map[string]int{"apple": 5, "cherry": 8} 37 | 38 | // Transform map 39 | transformed := maputils.Map(data, func(k string, v int) (string, string) { 40 | return k, fmt.Sprintf("count: %d", v) 41 | }) 42 | // Result: map[string]string{"apple": "count: 5", ...} 43 | 44 | // Merge maps 45 | m1 := map[string]int{"a": 1, "b": 2} 46 | m2 := map[string]int{"b": 3, "c": 4} 47 | merged := maputils.Merge(m1, m2) 48 | // Result: map[string]int{"a": 1, "b": 3, "c": 4} 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/stringutils/stringify.md: -------------------------------------------------------------------------------- 1 | # Stringify 2 | 3 | `func Stringify(value any, opts ...Options) string` 4 | 5 | Stringify receives an arbitrary value and converts it into a string. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/stringutils" 14 | ) 15 | 16 | func main() { 17 | value := 1000 18 | 19 | result := stringutils.Stringify(value) // "1000" 20 | 21 | fmt.Print(result) // "1000" 22 | } 23 | ``` 24 | 25 | Stringify also accepts an options object with the following properties: 26 | 27 | - `NilFormat`: the string format for nil values, defaults to "". 28 | - `NilMapFormat`: the string format for nil map objects, defaults to "{}". 29 | - `NilSliceFormat`: the string format for nil slice objects, defaults to "[]". 30 | - `Base`: a number between 2-36 ad the base when converting ints and uints to strings, defaults to Base 10. 31 | - `Precision`: number of digits to include when converting floats and complex numbers to strings, defaults to 2. 32 | - `Format`: the number notation format, using the stlib `FTOA` functionalities, defaults to 'f': 33 | - 'b' (-ddddp±ddd, a binary exponent), 34 | - 'e' (-d.dddde±dd, a decimal exponent), 35 | - 'E' (-d.ddddE±dd, a decimal exponent), 36 | - 'f' (-ddd.dddd, no exponent), 37 | - 'g' ('e' for large exponents, 'f' otherwise), 38 | - 'G' ('E' for large exponents, 'f' otherwise), 39 | - 'x' (-0xd.ddddp±ddd, a hexadecimal fraction and binary exponent), or 40 | - 'X' (-0Xd.ddddP±ddd, a hexadecimal fraction and binary exponent). 41 | -------------------------------------------------------------------------------- /docs/css/extra.css: -------------------------------------------------------------------------------- 1 | /* Custom styles for Go Utils documentation */ 2 | 3 | [data-md-color-scheme="default"] { 4 | --md-default-bg-color: #f4f4f4; /* Softer light background */ 5 | --md-default-fg-color: #212121; /* Keep text dark for contrast */ 6 | } 7 | 8 | [data-md-color-scheme="slate"] { 9 | --md-default-bg-color: #121212; /* Darker background */ 10 | --md-default-fg-color: #e0e0e0; /* Light text for good contrast */ 11 | --md-accent-fg-color: #ffcc00; /* Accent in yellow/amber for contrast */ 12 | --md-code-bg-color: #1e1e1e; /* Dark code block background */ 13 | } 14 | 15 | /* Improve code readability */ 16 | .codehilite pre, 17 | .highlight pre { 18 | padding: 1em; 19 | border-radius: 4px; 20 | } 21 | 22 | /* Make function signatures stand out */ 23 | h3 code { 24 | font-weight: bold; 25 | } 26 | 27 | /* Custom admonition styles */ 28 | .md-typeset .admonition.tip, 29 | .md-typeset details.tip { 30 | border-color: var(--md-accent-fg-color); 31 | } 32 | 33 | .md-typeset .tip > .admonition-title, 34 | .md-typeset .tip > summary { 35 | background-color: var(--md-accent-fg-color--transparent); 36 | } 37 | 38 | .md-typeset .tip > .admonition-title::before, 39 | .md-typeset .tip > summary::before { 40 | background-color: var(--md-accent-fg-color); 41 | } 42 | 43 | /* Custom footer styling */ 44 | .md-footer-meta { 45 | background-color: var(--md-primary-fg-color--dark); 46 | } 47 | 48 | /* Logo sizing */ 49 | .md-header__button.md-logo img { 50 | height: 1.6rem; 51 | } 52 | 53 | /* Custom link color on hover */ 54 | a:hover { 55 | color: var(--md-accent-fg-color); 56 | } 57 | -------------------------------------------------------------------------------- /docs/structutils/index.md: -------------------------------------------------------------------------------- 1 | # structutils 2 | 3 | Reflection-based struct utilities with struct tag support. 4 | 5 | ## Overview 6 | 7 | The `structutils` package provides utilities for working with structs through reflection, including conversion to maps, iteration over fields, and struct tag-aware operations. 8 | 9 | ## Functions 10 | 11 | **Conversion**: ToMap 12 | **Iteration**: ForEach 13 | **Inspection**: Fields, Values, FieldNames, HasField, GetField 14 | 15 | ## Example 16 | 17 | ```go 18 | import "github.com/Goldziher/go-utils/structutils" 19 | 20 | type Config struct { 21 | Host string `json:"host" yaml:"host"` 22 | Port int `json:"port" yaml:"port"` 23 | Database string `json:"database" yaml:"database"` 24 | Password string `json:"-"` // Omit from JSON 25 | } 26 | 27 | cfg := Config{ 28 | Host: "localhost", 29 | Port: 5432, 30 | Database: "mydb", 31 | Password: "secret", 32 | } 33 | 34 | // Convert to map (respects struct tags) 35 | jsonMap := structutils.ToMap(cfg, "json") 36 | // Result: map[string]any{ 37 | // "host": "localhost", 38 | // "port": 5432, 39 | // "database": "mydb", 40 | // // Password omitted due to json:"-" 41 | // } 42 | 43 | yamlMap := structutils.ToMap(cfg, "yaml") 44 | // Result: map[string]any{ 45 | // "host": "localhost", 46 | // "port": 5432, 47 | // "database": "mydb", 48 | // "Password": "secret", // Included as field name 49 | // } 50 | 51 | // Iterate over fields 52 | structutils.ForEach(cfg, func(key string, value any, tag reflect.StructTag) { 53 | fmt.Printf("%s: %v (json:%s)\n", key, value, tag.Get("json")) 54 | }) 55 | 56 | // Get field names with tag support 57 | fieldNames := structutils.FieldNames(cfg, "json") 58 | // Result: []string{"host", "port", "database"} 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/urlutils/index.md: -------------------------------------------------------------------------------- 1 | # urlutils 2 | 3 | URL parsing and query string builder utilities. 4 | 5 | ## Overview 6 | 7 | The `urlutils` package provides utilities for working with URLs and building query strings from maps and structs with struct tag support. 8 | 9 | ## Functions 10 | 11 | **Query Builders**: QueryStringifyMap, QueryStringifyStruct 12 | **URL Parsing**: Parse, MustParse 13 | **URL Inspection**: IsAbsolute, GetDomain, GetScheme, GetPath 14 | 15 | ## Example 16 | 17 | ```go 18 | import "github.com/Goldziher/go-utils/urlutils" 19 | 20 | // Build query string from map 21 | params := map[string]any{ 22 | "search": "golang", 23 | "page": 1, 24 | "tags": []string{"generics", "utils"}, 25 | "active": true, 26 | } 27 | query := urlutils.QueryStringifyMap(params) 28 | // Result: "active=true&page=1&search=golang&tags=generics&tags=utils" 29 | 30 | // Build query string from struct with tags 31 | type SearchQuery struct { 32 | Query string `qs:"q"` 33 | Page int `qs:"page"` 34 | PageSize int `qs:"page_size"` 35 | Tags []string `qs:"tag"` 36 | Active bool `qs:"active"` 37 | } 38 | 39 | q := SearchQuery{ 40 | Query: "golang", 41 | Page: 1, 42 | PageSize: 20, 43 | Tags: []string{"generics", "utils"}, 44 | Active: true, 45 | } 46 | query := urlutils.QueryStringifyStruct(q, "qs") 47 | // Result: "active=true&page=1&page_size=20&q=golang&tag=generics&tag=utils" 48 | 49 | // URL parsing and inspection 50 | u, err := urlutils.Parse("https://example.com:8080/path?key=value") 51 | if err == nil { 52 | domain := urlutils.GetDomain("https://example.com:8080/path") // "example.com:8080" 53 | scheme := urlutils.GetScheme("https://example.com/path") // "https" 54 | path := urlutils.GetPath("https://example.com/path/to/page") // "/path/to/page" 55 | isAbs := urlutils.IsAbsolute("https://example.com") // true 56 | } 57 | 58 | // Panic on parse error (use when URL is known to be valid) 59 | u := urlutils.MustParse("https://example.com/path") 60 | ``` 61 | -------------------------------------------------------------------------------- /docs/structutils/toMap.md: -------------------------------------------------------------------------------- 1 | # ToMap 2 | 3 | `func ToMap[T any](structInstance T, structTags ...string) map[string]any` 4 | 5 | ToMap takes a struct and converts it to into an instance of `map[string]any`. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "reflect" 13 | 14 | "github.com/Goldziher/go-utils/structutils" 15 | ) 16 | 17 | type Person struct { 18 | FirstName string 19 | LastName string 20 | Age int `myTag:"myValue"` 21 | } 22 | 23 | func main() { 24 | personInstance := Person{ 25 | FirstName: "Moishe", 26 | LastName: "Zuchmir", 27 | Age: 100, 28 | } 29 | 30 | personMap := structutils.ToMap(personInstance) 31 | 32 | fmt.Print(personMap) 33 | // { "FirstName": "Moishe", "LastName": "Zuchmir", "Age": 100 } 34 | } 35 | ``` 36 | 37 | You can also pass in struct tags as an optional argument: 38 | 39 | ```go 40 | package main 41 | 42 | import ( 43 | "fmt" 44 | "reflect" 45 | 46 | "github.com/Goldziher/go-utils/structutils" 47 | ) 48 | 49 | type Person struct { 50 | FirstName string 51 | LastName string 52 | Age int `myTag:"myValue"` 53 | } 54 | 55 | func main() { 56 | personInstance := Person{ 57 | FirstName: "Moishe", 58 | LastName: "Zuchmir", 59 | Age: 100, 60 | } 61 | 62 | personMap := structutils.ToMap(personInstance, "myTag") 63 | 64 | fmt.Print(personMap) 65 | // { "FirstName": "Moishe", "LastName": "Zuchmir", "myTag": 100 } 66 | } 67 | ``` 68 | 69 | To omit a value, use the standard `"-"` struct tag value: 70 | 71 | ```go 72 | package main 73 | 74 | import ( 75 | "fmt" 76 | "reflect" 77 | 78 | "github.com/Goldziher/go-utils/structutils" 79 | ) 80 | 81 | type Person struct { 82 | FirstName string 83 | LastName string 84 | Age int `myTag:"-"` 85 | } 86 | 87 | func main() { 88 | personInstance := Person{ 89 | FirstName: "Moishe", 90 | LastName: "Zuchmir", 91 | Age: 100, 92 | } 93 | 94 | personMap := structutils.ToMap(personInstance, "myTag") 95 | 96 | fmt.Print(personMap) 97 | // { "FirstName": "Moishe", "LastName": "Zuchmir" } 98 | } 99 | ``` 100 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | pull_request: 4 | branches: 5 | - "main" 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | validate: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v6 16 | - name: Set up Python 17 | uses: actions/setup-python@v6 18 | with: 19 | python-version: "3.14" 20 | - name: Install uv 21 | uses: astral-sh/setup-uv@v7 22 | with: 23 | enable-cache: true 24 | - name: Setup Golang 25 | uses: actions/setup-go@v6 26 | with: 27 | go-version: "1.25" 28 | - name: Install prek and ai-rulez 29 | run: | 30 | uv tool install prek 31 | uv tool install ai-rulez 32 | echo "$HOME/.local/bin" >> $GITHUB_PATH 33 | - name: Load Cached Prek Dependencies 34 | id: cached-prek-dependencies 35 | uses: actions/cache@v5 36 | with: 37 | path: ~/.cache/prek/ 38 | key: prek|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} 39 | - name: Execute Prek 40 | run: prek run --show-diff-on-failure --color=always --all-files 41 | test: 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v6 45 | - uses: actions/setup-go@v6 46 | with: 47 | go-version: "1.25" 48 | - name: Load cached dependencies 49 | id: cached-go-dependencies 50 | uses: actions/cache@v5 51 | with: 52 | path: | 53 | ~/.cache/go-build 54 | ~/go/pkg/mod 55 | key: go-${{ inputs.go_version }}-v1.0-${{ hashFiles('**/go.sum') }} 56 | - name: Install dependencies 57 | if: steps.cached-go-dependencies.outputs.cache-hit != 'true' 58 | shell: bash 59 | run: go get -v -t ./... 60 | - name: Run Tests 61 | run: go test -coverprofile=coverage.out ./... 62 | - name: Archive code coverage results 63 | uses: actions/upload-artifact@v6 64 | with: 65 | name: coverage-report 66 | path: ./coverage.out 67 | -------------------------------------------------------------------------------- /docs/urlutils/queryStringifyStruct.md: -------------------------------------------------------------------------------- 1 | # QueryStringifyStruct 2 | 3 | `func QueryStringifyStruct[T any](values T, structTags ...string) string` 4 | 5 | Creates a query string from a given struct instance. Takes struct tag names as optional parameters. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Goldziher/go-utils/urlutils" 14 | ) 15 | 16 | func main() { 17 | values := struct { 18 | User string 19 | Active bool 20 | Age int `qs:"age"` 21 | Friends []int 22 | }{ 23 | User: "moishe", 24 | Active: true, 25 | Age: 100, 26 | Friends: []int{1, 2, 3, 4, 5, 6}, 27 | } 28 | 29 | result := urlutils.QueryStringifyStruct(values, "qs") 30 | 31 | fmt.Print(result) // "Active=true&Friends=1&Friends=2&Friends=3&Friends=4&Friends=5&Friends=6&User=moishe&age=100" 32 | } 33 | ``` 34 | 35 | You can pass as many struct tags as you deem necessary, for example the following will also work: 36 | 37 | ```go 38 | package main 39 | 40 | import ( 41 | "fmt" 42 | 43 | "github.com/Goldziher/go-utils/urlutils" 44 | ) 45 | 46 | func main() { 47 | values := struct { 48 | User string 49 | Active bool `json:"active"` 50 | Age int `qs:"age"` 51 | Friends []int 52 | }{ 53 | User: "moishe", 54 | Active: true, 55 | Age: 100, 56 | Friends: []int{1, 2, 3, 4, 5, 6}, 57 | } 58 | 59 | result := urlutils.QueryStringifyStruct(values, "json", "qs") 60 | 61 | fmt.Print(result) // "Friends=1&Friends=2&Friends=3&Friends=4&Friends=5&Friends=6&User=moishe&active=true&age=100" 62 | } 63 | ``` 64 | 65 | If you want to ignore a field, simply use the conventional tag value of `"-"`: 66 | 67 | ```go 68 | package main 69 | 70 | import ( 71 | "fmt" 72 | 73 | "github.com/Goldziher/go-utils/urlutils" 74 | ) 75 | 76 | func main() { 77 | values := struct { 78 | User string 79 | Active bool 80 | Age int `qs:"-"` 81 | Friends []int 82 | }{ 83 | User: "moishe", 84 | Active: true, 85 | Age: 100, 86 | Friends: []int{1, 2, 3, 4, 5, 6}, 87 | } 88 | 89 | result := urlutils.QueryStringifyStruct(values, "qs") 90 | 91 | fmt.Print(result) // "Active=true&Friends=1&Friends=2&Friends=3&Friends=4&Friends=5&Friends=6&User=moishe" 92 | } 93 | ``` 94 | -------------------------------------------------------------------------------- /docs/stringutils/index.md: -------------------------------------------------------------------------------- 1 | # stringutils 2 | 3 | String manipulation and type-safe conversion utilities. 4 | 5 | ## Overview 6 | 7 | The `stringutils` package provides utilities for string manipulation and a powerful type-safe `Stringify` function that converts any value to a string with configurable options. 8 | 9 | ## Functions 10 | 11 | **Type Conversion**: Stringify (with Options) 12 | **Case Conversion**: Capitalize, ToCamelCase, ToSnakeCase, ToKebabCase 13 | **Padding**: PadLeft, PadRight 14 | **Manipulation**: Reverse, Truncate, RemoveWhitespace, EllipsisMiddle 15 | **Utilities**: Contains, SplitAndTrim, JoinNonEmpty, DefaultIfEmpty 16 | 17 | ## Example 18 | 19 | ```go 20 | import "github.com/Goldziher/go-utils/stringutils" 21 | 22 | // Type-safe stringify with options 23 | str := stringutils.Stringify(42) // "42" 24 | hex := stringutils.Stringify(42, stringutils.Options{Base: 16}) // "2a" 25 | float := stringutils.Stringify(3.14159, stringutils.Options{Precision: 2}) // "3.14" 26 | 27 | // Stringify complex types 28 | m := map[string]int{"a": 1, "b": 2} 29 | str := stringutils.Stringify(m) // "{a: 1, b: 2}" 30 | 31 | // String manipulation 32 | capitalized := stringutils.Capitalize("hello world") // "Hello world" 33 | padded := stringutils.PadLeft("42", "0", 5) // "00042" 34 | camel := stringutils.ToCamelCase("hello_world") // "helloWorld" 35 | snake := stringutils.ToSnakeCase("HelloWorld") // "hello_world" 36 | kebab := stringutils.ToKebabCase("HelloWorld") // "hello-world" 37 | 38 | // Utilities 39 | truncated := stringutils.Truncate("very long string", 10, "...") // "very long ..." 40 | cleaned := stringutils.RemoveWhitespace("hello world") // "helloworld" 41 | parts := stringutils.SplitAndTrim("a, b, c", ",") // []string{"a", "b", "c"} 42 | ``` 43 | 44 | ## Stringify Options 45 | 46 | ```go 47 | type Options struct { 48 | Base int // Number base for integers (2-36), default 10 49 | Format byte // Float format ('f', 'e', 'E', 'g', 'G'), default 'f' 50 | Precision int // Float precision, default 2 51 | NilFormat string // Format for nil values, default "" 52 | NilMapFormat string // Format for nil maps, default "{}" 53 | NilSliceFormat string // Format for nil slices, default "[]" 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/dateutils/index.md: -------------------------------------------------------------------------------- 1 | # dateutils 2 | 3 | Time and date utilities for business logic and date manipulation. 4 | 5 | ## Overview 6 | 7 | The `dateutils` package provides utilities for working with dates and times, including business day calculations, date range operations, and common date manipulations. 8 | 9 | ## Functions 10 | 11 | **Date Boundaries**: Floor, Ceil, StartOfDay, EndOfDay, StartOfWeek, EndOfWeek 12 | **Month Operations**: GetFirstDayOfMonth, GetLastDayOfMonth, DaysInMonth 13 | **Date Ranges**: Overlap, DaysBetween 14 | **Business Logic**: AddBusinessDays, IsWeekend, IsWeekday 15 | **Age Calculation**: Age, AgeAt 16 | **Parsing**: ParseDate, ParseDateWithLayout, MustParseDate, MustParseDateWithLayout 17 | **Comparison**: IsSameDay, IsSameMonth 18 | 19 | ## Example 20 | 21 | ```go 22 | import ( 23 | "time" 24 | "github.com/Goldziher/go-utils/dateutils" 25 | ) 26 | 27 | now := time.Now() 28 | 29 | // Date boundaries 30 | startOfDay := dateutils.StartOfDay(now) // Today at 00:00:00 31 | endOfDay := dateutils.EndOfDay(now) // Today at 23:59:59.999999999 32 | startOfWeek := dateutils.StartOfWeek(now) // This Sunday at 00:00:00 33 | 34 | // Business day calculations 35 | nextWeek := dateutils.AddBusinessDays(now, 5) // 5 business days from now 36 | isWeekend := dateutils.IsWeekend(now) // true if Saturday/Sunday 37 | 38 | // Date range operations 39 | start1 := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) 40 | end1 := time.Date(2024, 1, 10, 0, 0, 0, 0, time.UTC) 41 | start2 := time.Date(2024, 1, 5, 0, 0, 0, 0, time.UTC) 42 | end2 := time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC) 43 | 44 | overlaps := dateutils.Overlap(start1, end1, start2, end2) // true 45 | 46 | // Age calculation 47 | birthdate := time.Date(1990, 5, 15, 0, 0, 0, 0, time.UTC) 48 | age := dateutils.Age(birthdate) // Current age in years 49 | 50 | // Month operations 51 | firstDay := dateutils.GetFirstDayOfMonth() // First day of current month 52 | lastDay := dateutils.GetLastDayOfMonth() // Last day of current month 53 | daysInMonth := dateutils.DaysInMonth(now) // Number of days in current month 54 | 55 | // Date comparison 56 | same := dateutils.IsSameDay(time.Now(), time.Now().Add(time.Hour)) // true 57 | sameMonth := dateutils.IsSameMonth(start1, start2) // true 58 | ``` 59 | -------------------------------------------------------------------------------- /mathutils/mathutils.go: -------------------------------------------------------------------------------- 1 | // This package includes utility functions for mathematical operations. 2 | // It provides helpers for common math operations with generic support. 3 | 4 | package mathutils 5 | 6 | import ( 7 | "cmp" 8 | 9 | "golang.org/x/exp/constraints" 10 | ) 11 | 12 | // Number is a constraint for numeric types. 13 | type Number interface { 14 | constraints.Integer | constraints.Float 15 | } 16 | 17 | // Clamp restricts a value to be within a specified range. 18 | // If value < min, returns min. If value > max, returns max. Otherwise returns value. 19 | func Clamp[T cmp.Ordered](value, min, max T) T { 20 | if value < min { 21 | return min 22 | } 23 | if value > max { 24 | return max 25 | } 26 | return value 27 | } 28 | 29 | // Abs returns the absolute value of an integer. 30 | // For unsigned types, returns the value as-is. 31 | func Abs[T Number](value T) T { 32 | if value < 0 { 33 | return -value 34 | } 35 | return value 36 | } 37 | 38 | // InRange checks if a value is within a range [min, max] (inclusive). 39 | func InRange[T cmp.Ordered](value, min, max T) bool { 40 | return value >= min && value <= max 41 | } 42 | 43 | // Average returns the average (mean) of all values in a slice. 44 | // Returns 0 for empty slice. 45 | func Average[T Number](values []T) float64 { 46 | if len(values) == 0 { 47 | return 0 48 | } 49 | var sum T 50 | for _, v := range values { 51 | sum += v 52 | } 53 | return float64(sum) / float64(len(values)) 54 | } 55 | 56 | // Gcd returns the greatest common divisor of two integers using Euclidean algorithm. 57 | func Gcd[T constraints.Integer](a, b T) T { 58 | for b != 0 { 59 | a, b = b, a%b 60 | } 61 | return Abs(a) 62 | } 63 | 64 | // Lcm returns the least common multiple of two integers. 65 | func Lcm[T constraints.Integer](a, b T) T { 66 | if a == 0 || b == 0 { 67 | return 0 68 | } 69 | return Abs(a*b) / Gcd(a, b) 70 | } 71 | 72 | // IsPrime checks if a number is prime. 73 | // Returns false for numbers less than 2. 74 | func IsPrime[T constraints.Integer](n T) bool { 75 | if n < 2 { 76 | return false 77 | } 78 | if n == 2 { 79 | return true 80 | } 81 | if n%2 == 0 { 82 | return false 83 | } 84 | 85 | // Check odd divisors up to sqrt(n) 86 | var i T = 3 87 | for i*i <= n { 88 | if n%i == 0 { 89 | return false 90 | } 91 | i += 2 92 | } 93 | return true 94 | } 95 | -------------------------------------------------------------------------------- /mathutils/mathutils_test.go: -------------------------------------------------------------------------------- 1 | package mathutils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Goldziher/go-utils/mathutils" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestClamp(t *testing.T) { 11 | assert.Equal(t, 5, mathutils.Clamp(5, 0, 10)) 12 | assert.Equal(t, 0, mathutils.Clamp(-5, 0, 10)) 13 | assert.Equal(t, 10, mathutils.Clamp(15, 0, 10)) 14 | assert.Equal(t, 2.5, mathutils.Clamp(2.5, 0.0, 5.0)) 15 | } 16 | 17 | func TestAbs(t *testing.T) { 18 | assert.Equal(t, 5, mathutils.Abs(5)) 19 | assert.Equal(t, 5, mathutils.Abs(-5)) 20 | assert.Equal(t, 0, mathutils.Abs(0)) 21 | assert.Equal(t, 3.5, mathutils.Abs(-3.5)) 22 | assert.Equal(t, uint(5), mathutils.Abs(uint(5))) 23 | } 24 | 25 | func TestInRange(t *testing.T) { 26 | assert.True(t, mathutils.InRange(5, 0, 10)) 27 | assert.True(t, mathutils.InRange(0, 0, 10)) 28 | assert.True(t, mathutils.InRange(10, 0, 10)) 29 | assert.False(t, mathutils.InRange(-1, 0, 10)) 30 | assert.False(t, mathutils.InRange(11, 0, 10)) 31 | } 32 | 33 | func TestAverage(t *testing.T) { 34 | assert.Equal(t, 3.0, mathutils.Average([]int{1, 2, 3, 4, 5})) 35 | assert.Equal(t, 0.0, mathutils.Average([]int{})) 36 | assert.Equal(t, 5.0, mathutils.Average([]float64{2.5, 7.5})) 37 | assert.Equal(t, -2.5, mathutils.Average([]int{-2, -3})) 38 | } 39 | 40 | func TestGcd(t *testing.T) { 41 | assert.Equal(t, 6, mathutils.Gcd(48, 18)) 42 | assert.Equal(t, 1, mathutils.Gcd(17, 13)) 43 | assert.Equal(t, 5, mathutils.Gcd(10, 5)) 44 | assert.Equal(t, 12, mathutils.Gcd(-12, 24)) 45 | assert.Equal(t, 7, mathutils.Gcd(7, 0)) 46 | } 47 | 48 | func TestLcm(t *testing.T) { 49 | assert.Equal(t, 12, mathutils.Lcm(4, 6)) 50 | assert.Equal(t, 221, mathutils.Lcm(17, 13)) 51 | assert.Equal(t, 10, mathutils.Lcm(10, 5)) 52 | assert.Equal(t, 0, mathutils.Lcm(0, 5)) 53 | assert.Equal(t, 0, mathutils.Lcm(5, 0)) 54 | } 55 | 56 | func TestIsPrime(t *testing.T) { 57 | assert.True(t, mathutils.IsPrime(2)) 58 | assert.True(t, mathutils.IsPrime(3)) 59 | assert.True(t, mathutils.IsPrime(5)) 60 | assert.True(t, mathutils.IsPrime(7)) 61 | assert.True(t, mathutils.IsPrime(11)) 62 | assert.True(t, mathutils.IsPrime(13)) 63 | assert.True(t, mathutils.IsPrime(17)) 64 | assert.True(t, mathutils.IsPrime(19)) 65 | 66 | assert.False(t, mathutils.IsPrime(0)) 67 | assert.False(t, mathutils.IsPrime(1)) 68 | assert.False(t, mathutils.IsPrime(4)) 69 | assert.False(t, mathutils.IsPrime(6)) 70 | assert.False(t, mathutils.IsPrime(8)) 71 | assert.False(t, mathutils.IsPrime(9)) 72 | assert.False(t, mathutils.IsPrime(10)) 73 | assert.False(t, mathutils.IsPrime(15)) 74 | assert.False(t, mathutils.IsPrime(21)) 75 | } 76 | -------------------------------------------------------------------------------- /docs/sliceutils/index.md: -------------------------------------------------------------------------------- 1 | # sliceutils 2 | 3 | Functional programming utilities for slices, inspired by JavaScript Array methods and LINQ. 4 | 5 | ## Overview 6 | 7 | The `sliceutils` package provides type-safe generic functions for slice manipulation. All functions work with any slice type through Go generics and accept callback functions following JavaScript's Array method conventions (value, index, slice). 8 | 9 | ## Categories 10 | 11 | **Functional Operations**: Map, Filter, Reduce, ForEach 12 | **Search Operations**: Find, FindIndex, FindIndexes, FindLastIndex 13 | **Predicates**: Some, Every 14 | **Set Operations**: Union, Intersection, Difference, Unique 15 | **Transformations**: Reverse, Flatten, FlatMap, Chunk, Pluck 16 | **LINQ-style**: GroupBy, Partition, DistinctBy, CountBy, MinBy, MaxBy 17 | **Utilities**: Remove, EnsureUniqueAndAppend, Sum 18 | 19 | ## When to use stdlib vs sliceutils 20 | 21 | **Use stdlib `slices` for**: 22 | - `slices.Index(slice, value)` - find index of value 23 | - `slices.Contains(slice, value)` - check if slice contains value 24 | - `slices.Clone(slice)` - copy a slice 25 | - `slices.Concat(slices...)` - merge slices 26 | - `slices.Sort(slice)` - sort a slice 27 | 28 | **Use sliceutils for**: 29 | - Functional patterns with callbacks (Map, Filter, Reduce) 30 | - LINQ-style operations (GroupBy, Partition, DistinctBy) 31 | - Complex search operations (FindIndex with predicate) 32 | - Set operations (Union, Intersection, Difference) 33 | 34 | ## Example 35 | 36 | ```go 37 | import "github.com/Goldziher/go-utils/sliceutils" 38 | 39 | numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 40 | 41 | // Functional patterns 42 | evens := sliceutils.Filter(numbers, func(v, i int, s []int) bool { 43 | return v%2 == 0 44 | }) 45 | 46 | doubled := sliceutils.Map(evens, func(v, i int, s []int) int { 47 | return v * 2 48 | }) 49 | 50 | sum := sliceutils.Reduce(doubled, func(acc, v, i int, s []int) int { 51 | return acc + v 52 | }, 0) 53 | 54 | // LINQ-style operations 55 | type User struct { 56 | Name string 57 | Age int 58 | Role string 59 | } 60 | 61 | users := []User{ 62 | {"Alice", 30, "admin"}, 63 | {"Bob", 25, "user"}, 64 | {"Charlie", 30, "user"}, 65 | } 66 | 67 | // Group by age 68 | byAge := sliceutils.GroupBy(users, func(u User) int { 69 | return u.Age 70 | }) 71 | 72 | // Partition by role 73 | admins, regularUsers := sliceutils.Partition(users, func(u User) bool { 74 | return u.Role == "admin" 75 | }) 76 | 77 | // Get distinct ages 78 | ages := sliceutils.Map(users, func(u User, i int, s []User) int { 79 | return u.Age 80 | }) 81 | uniqueAges := sliceutils.Unique(ages) 82 | ``` 83 | -------------------------------------------------------------------------------- /ptrutils/ptrutils.go: -------------------------------------------------------------------------------- 1 | // This package includes utility functions for working with pointers. 2 | // It provides helpers for creating pointers and safely dereferencing them. 3 | 4 | package ptr 5 | 6 | // To returns a pointer to the given value. 7 | // This is useful for creating pointers to literals or values in a single expression. 8 | func To[T any](v T) *T { 9 | return &v 10 | } 11 | 12 | // Deref dereferences ptr and returns the value it points to if not nil, or else 13 | // returns def (the default value). 14 | // This provides safe pointer dereferencing without nil checks. 15 | func Deref[T any](ptr *T, def T) T { 16 | if ptr != nil { 17 | return *ptr 18 | } 19 | return def 20 | } 21 | 22 | // Equal compares two pointers for equality. 23 | // Returns true if both are nil or both point to equal values. 24 | // Returns false if one is nil and the other is not. 25 | func Equal[T comparable](a, b *T) bool { 26 | if a == nil && b == nil { 27 | return true 28 | } 29 | if a == nil || b == nil { 30 | return false 31 | } 32 | return *a == *b 33 | } 34 | 35 | // Coalesce returns the first non-nil pointer from the list. 36 | // Returns nil if all pointers are nil. 37 | func Coalesce[T any](ptrs ...*T) *T { 38 | for _, ptr := range ptrs { 39 | if ptr != nil { 40 | return ptr 41 | } 42 | } 43 | return nil 44 | } 45 | 46 | // IsNil checks if a pointer is nil. 47 | // This is a convenience function for readability. 48 | func IsNil[T any](ptr *T) bool { 49 | return ptr == nil 50 | } 51 | 52 | // ToSlice converts a pointer to a slice. 53 | // Returns an empty slice if ptr is nil, otherwise returns a slice with the single element. 54 | func ToSlice[T any](ptr *T) []T { 55 | if ptr == nil { 56 | return []T{} 57 | } 58 | return []T{*ptr} 59 | } 60 | 61 | // DerefSlice dereferences all pointers in a slice, using def for nil pointers. 62 | // Returns a new slice with dereferenced values. 63 | func DerefSlice[T any](ptrs []*T, def T) []T { 64 | result := make([]T, len(ptrs)) 65 | for i, ptr := range ptrs { 66 | result[i] = Deref(ptr, def) 67 | } 68 | return result 69 | } 70 | 71 | // NonNilSlice returns a new slice containing only non-nil pointers from the input. 72 | func NonNilSlice[T any](ptrs []*T) []*T { 73 | result := make([]*T, 0, len(ptrs)) 74 | for _, ptr := range ptrs { 75 | if ptr != nil { 76 | result = append(result, ptr) 77 | } 78 | } 79 | return result 80 | } 81 | 82 | // ToPointerSlice converts a slice of values to a slice of pointers. 83 | // Each element in the result points to the corresponding element in the input. 84 | func ToPointerSlice[T any](values []T) []*T { 85 | result := make([]*T, len(values)) 86 | for i := range values { 87 | result[i] = &values[i] 88 | } 89 | return result 90 | } 91 | -------------------------------------------------------------------------------- /urlutils/urlutils.go: -------------------------------------------------------------------------------- 1 | // This package includes utility functions for URL and query string manipulation. 2 | // It provides helpers for building query strings from maps and structs. 3 | 4 | package urlutils 5 | 6 | import ( 7 | "net/url" 8 | "reflect" 9 | 10 | "github.com/Goldziher/go-utils/stringutils" 11 | ) 12 | 13 | // QueryStringifyMap creates a query string from a given map instance. 14 | func QueryStringifyMap[K comparable, V any](values map[K]V) string { 15 | query := url.Values{} 16 | 17 | for key, value := range values { 18 | stringifiedKey := stringutils.Stringify(key) 19 | if reflect.TypeOf(value).Kind() == reflect.Slice { 20 | s := reflect.ValueOf(value) 21 | if s.IsNil() { 22 | query.Add(stringifiedKey, "") 23 | } 24 | 25 | for i := 0; i < s.Len(); i++ { 26 | query.Add(stringifiedKey, stringutils.Stringify(s.Index(i).Interface())) 27 | } 28 | } else { 29 | query.Add(stringifiedKey, stringutils.Stringify(value)) 30 | } 31 | } 32 | 33 | return query.Encode() 34 | } 35 | 36 | // QueryStringifyStruct creates a query string from a given struct instance. Takes struct tag names as optional parameters. 37 | func QueryStringifyStruct[T any](values T, structTags ...string) string { 38 | query := url.Values{} 39 | 40 | typeOf := reflect.TypeOf(values) 41 | valueOf := reflect.ValueOf(values) 42 | fields := reflect.VisibleFields(typeOf) 43 | 44 | for _, field := range fields { 45 | key := field.Name 46 | 47 | if field.Tag != "" { 48 | for _, s := range structTags { 49 | if tagValue, isPresent := field.Tag.Lookup(s); isPresent && tagValue != "" { 50 | key = tagValue 51 | break 52 | } 53 | } 54 | } 55 | 56 | if key == "-" { 57 | continue 58 | } 59 | 60 | value := valueOf.FieldByName(field.Name) 61 | 62 | if value.Kind() == reflect.Slice { 63 | if value.IsNil() { 64 | query.Add(key, "") 65 | } 66 | 67 | for i := 0; i < value.Len(); i++ { 68 | query.Add(key, stringutils.Stringify(value.Index(i).Interface())) 69 | } 70 | } else { 71 | query.Add(key, stringutils.Stringify(value.Interface())) 72 | } 73 | } 74 | 75 | return query.Encode() 76 | } 77 | 78 | // Parse parses a raw URL and returns a URL struct or an error. 79 | // This is a convenience wrapper around url.Parse. 80 | func Parse(rawURL string) (*url.URL, error) { 81 | return url.Parse(rawURL) 82 | } 83 | 84 | // MustParse parses a raw URL and panics if parsing fails. 85 | // Use this when you're certain the URL is valid. 86 | func MustParse(rawURL string) *url.URL { 87 | u, err := url.Parse(rawURL) 88 | if err != nil { 89 | panic(err) 90 | } 91 | return u 92 | } 93 | 94 | // IsAbsolute returns true if the URL is absolute (has a scheme). 95 | func IsAbsolute(rawURL string) bool { 96 | u, err := url.Parse(rawURL) 97 | if err != nil { 98 | return false 99 | } 100 | return u.IsAbs() 101 | } 102 | 103 | // GetDomain extracts the domain (host) from a URL string. 104 | // Returns empty string if parsing fails or no host is present. 105 | func GetDomain(rawURL string) string { 106 | u, err := url.Parse(rawURL) 107 | if err != nil { 108 | return "" 109 | } 110 | return u.Host 111 | } 112 | 113 | // GetScheme extracts the scheme from a URL string. 114 | // Returns empty string if parsing fails or no scheme is present. 115 | func GetScheme(rawURL string) string { 116 | u, err := url.Parse(rawURL) 117 | if err != nil { 118 | return "" 119 | } 120 | return u.Scheme 121 | } 122 | 123 | // GetPath extracts the path from a URL string. 124 | // Returns empty string if parsing fails. 125 | func GetPath(rawURL string) string { 126 | u, err := url.Parse(rawURL) 127 | if err != nil { 128 | return "" 129 | } 130 | return u.Path 131 | } 132 | -------------------------------------------------------------------------------- /structutils/structutils.go: -------------------------------------------------------------------------------- 1 | // This package includes utility functions for working with structs using reflection. 2 | // It provides helpers for iterating over struct fields and converting structs to maps. 3 | 4 | package structutils 5 | 6 | import "reflect" 7 | 8 | // ForEach given a struct, calls the passed in function with each visible struct field's name, value and tag. 9 | func ForEach[T any](structInstance T, function func(key string, value any, tag reflect.StructTag)) { 10 | typeOf := reflect.TypeOf(structInstance) 11 | valueOf := reflect.ValueOf(structInstance) 12 | fields := reflect.VisibleFields(typeOf) 13 | 14 | for _, field := range fields { 15 | value := valueOf.FieldByName(field.Name) 16 | function(field.Name, value.Interface(), field.Tag) 17 | } 18 | } 19 | 20 | // ToMap given a struct, converts it to a map[string]any. 21 | // This function also takes struct tag names as optional parameters - if passed in, the struct tags will be used to remap or omit values. 22 | func ToMap[T any](structInstance T, structTags ...string) map[string]any { 23 | output := make(map[string]any, 0) 24 | 25 | ForEach(structInstance, func(key string, value any, tag reflect.StructTag) { 26 | if tag != "" { 27 | for _, s := range structTags { 28 | if tagValue, isPresent := tag.Lookup(s); isPresent && tagValue != "" { 29 | key = tagValue 30 | break 31 | } 32 | } 33 | } 34 | if key != "-" { 35 | output[key] = value 36 | } 37 | }) 38 | 39 | return output 40 | } 41 | 42 | // Fields returns a slice of field names from a struct. 43 | func Fields[T any](structInstance T) []string { 44 | typeOf := reflect.TypeOf(structInstance) 45 | fields := reflect.VisibleFields(typeOf) 46 | result := make([]string, len(fields)) 47 | for i, field := range fields { 48 | result[i] = field.Name 49 | } 50 | return result 51 | } 52 | 53 | // Values returns a slice of field values from a struct. 54 | func Values[T any](structInstance T) []any { 55 | typeOf := reflect.TypeOf(structInstance) 56 | valueOf := reflect.ValueOf(structInstance) 57 | fields := reflect.VisibleFields(typeOf) 58 | result := make([]any, len(fields)) 59 | for i, field := range fields { 60 | value := valueOf.FieldByName(field.Name) 61 | result[i] = value.Interface() 62 | } 63 | return result 64 | } 65 | 66 | // HasField checks if a struct has a field with the given name. 67 | func HasField[T any](structInstance T, fieldName string) bool { 68 | typeOf := reflect.TypeOf(structInstance) 69 | _, found := typeOf.FieldByName(fieldName) 70 | return found 71 | } 72 | 73 | // GetField retrieves the value of a field by name. 74 | // Returns the value and true if found, zero value and false if not found. 75 | func GetField[T any](structInstance T, fieldName string) (any, bool) { 76 | valueOf := reflect.ValueOf(structInstance) 77 | field := valueOf.FieldByName(fieldName) 78 | if !field.IsValid() { 79 | return nil, false 80 | } 81 | return field.Interface(), true 82 | } 83 | 84 | // FieldNames returns field names, optionally using struct tags for naming. 85 | // If struct tags are provided, uses the first matching tag value as the field name. 86 | // Omits fields with tag value "-". 87 | func FieldNames[T any](structInstance T, structTags ...string) []string { 88 | typeOf := reflect.TypeOf(structInstance) 89 | fields := reflect.VisibleFields(typeOf) 90 | result := make([]string, 0, len(fields)) 91 | 92 | for _, field := range fields { 93 | name := field.Name 94 | 95 | if field.Tag != "" && len(structTags) > 0 { 96 | for _, tag := range structTags { 97 | if tagValue, isPresent := field.Tag.Lookup(tag); isPresent && tagValue != "" { 98 | name = tagValue 99 | break 100 | } 101 | } 102 | } 103 | 104 | if name != "-" { 105 | result = append(result, name) 106 | } 107 | } 108 | 109 | return result 110 | } 111 | -------------------------------------------------------------------------------- /structutils/structutils_test.go: -------------------------------------------------------------------------------- 1 | package structutils_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/Goldziher/go-utils/structutils" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type TestStruct struct { 12 | First string 13 | Second int 14 | Third bool `struct:"third"` 15 | } 16 | 17 | func TestForEach(t *testing.T) { 18 | instance := TestStruct{ 19 | "moishe", 20 | 22, 21 | true, 22 | } 23 | structutils.ForEach(instance, func(key string, value any, tag reflect.StructTag) { 24 | switch key { 25 | case "First": 26 | assert.Equal(t, "moishe", value.(string)) 27 | assert.Zero(t, tag) 28 | case "Second": 29 | assert.Equal(t, 22, value.(int)) 30 | assert.Zero(t, tag) 31 | case "Third": 32 | assert.Equal(t, true, value.(bool)) 33 | assert.Equal(t, "third", tag.Get("struct")) 34 | } 35 | }) 36 | } 37 | 38 | func TestToMap(t *testing.T) { 39 | instance := TestStruct{ 40 | "moishe", 41 | 22, 42 | true, 43 | } 44 | assert.Equal(t, map[string]any{ 45 | "First": "moishe", 46 | "Second": 22, 47 | "Third": true, 48 | }, structutils.ToMap(instance)) 49 | 50 | assert.Equal(t, map[string]any{ 51 | "First": "moishe", 52 | "Second": 22, 53 | "third": true, 54 | }, structutils.ToMap(instance, "struct")) 55 | } 56 | 57 | func TestFields(t *testing.T) { 58 | instance := TestStruct{ 59 | "moishe", 60 | 22, 61 | true, 62 | } 63 | fields := structutils.Fields(instance) 64 | assert.Len(t, fields, 3) 65 | assert.Contains(t, fields, "First") 66 | assert.Contains(t, fields, "Second") 67 | assert.Contains(t, fields, "Third") 68 | } 69 | 70 | func TestValues(t *testing.T) { 71 | instance := TestStruct{ 72 | "moishe", 73 | 22, 74 | true, 75 | } 76 | values := structutils.Values(instance) 77 | assert.Len(t, values, 3) 78 | assert.Contains(t, values, "moishe") 79 | assert.Contains(t, values, 22) 80 | assert.Contains(t, values, true) 81 | } 82 | 83 | func TestHasField(t *testing.T) { 84 | instance := TestStruct{ 85 | "moishe", 86 | 22, 87 | true, 88 | } 89 | assert.True(t, structutils.HasField(instance, "First")) 90 | assert.True(t, structutils.HasField(instance, "Second")) 91 | assert.True(t, structutils.HasField(instance, "Third")) 92 | assert.False(t, structutils.HasField(instance, "Nonexistent")) 93 | } 94 | 95 | func TestGetField(t *testing.T) { 96 | instance := TestStruct{ 97 | "moishe", 98 | 22, 99 | true, 100 | } 101 | 102 | value, found := structutils.GetField(instance, "First") 103 | assert.True(t, found) 104 | assert.Equal(t, "moishe", value) 105 | 106 | value, found = structutils.GetField(instance, "Second") 107 | assert.True(t, found) 108 | assert.Equal(t, 22, value) 109 | 110 | value, found = structutils.GetField(instance, "Third") 111 | assert.True(t, found) 112 | assert.Equal(t, true, value) 113 | 114 | value, found = structutils.GetField(instance, "Nonexistent") 115 | assert.False(t, found) 116 | assert.Nil(t, value) 117 | } 118 | 119 | func TestFieldNames(t *testing.T) { 120 | instance := TestStruct{ 121 | "moishe", 122 | 22, 123 | true, 124 | } 125 | 126 | // Without tags 127 | names := structutils.FieldNames(instance) 128 | assert.Len(t, names, 3) 129 | assert.Contains(t, names, "First") 130 | assert.Contains(t, names, "Second") 131 | assert.Contains(t, names, "Third") 132 | 133 | // With tags 134 | names = structutils.FieldNames(instance, "struct") 135 | assert.Len(t, names, 3) 136 | assert.Contains(t, names, "First") 137 | assert.Contains(t, names, "Second") 138 | assert.Contains(t, names, "third") // Uses tag value 139 | } 140 | 141 | func TestFieldNamesWithOmit(t *testing.T) { 142 | type OmitStruct struct { 143 | Keep string 144 | Omit string `json:"-"` 145 | Rename string `json:"renamed"` 146 | } 147 | 148 | instance := OmitStruct{ 149 | Keep: "value", 150 | Omit: "omitted", 151 | Rename: "renamed_value", 152 | } 153 | 154 | names := structutils.FieldNames(instance, "json") 155 | assert.Len(t, names, 2) 156 | assert.Contains(t, names, "Keep") 157 | assert.Contains(t, names, "renamed") 158 | assert.NotContains(t, names, "Omit") 159 | } 160 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Go Utils 2 | 3 | Functional programming utilities for Go 1.21+ designed to complement the standard library's `slices`, `maps`, and `cmp` packages. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | go get -u github.com/Goldziher/go-utils 9 | ``` 10 | 11 | ## Design Philosophy 12 | 13 | **Complementary to stdlib**: Use `slices.Index`, `slices.Contains`, `slices.Clone` for basic operations. Use this library for functional patterns (Map, Filter, Reduce), LINQ-style operations (GroupBy, Partition), and utilities not available in stdlib. 14 | 15 | **Type-safe generics**: All functions leverage Go 1.21+ generics with appropriate constraints (`comparable`, `cmp.Ordered`, etc.) for compile-time type safety. 16 | 17 | **Immutable by default**: Functions don't mutate inputs unless explicitly named (e.g., `Remove` creates new slices). 18 | 19 | **100% test coverage**: Every function has comprehensive test coverage maintained by CI. 20 | 21 | ## Packages 22 | 23 | ### sliceutils 24 | Functional operations and LINQ-style utilities for slices. 25 | 26 | ```go 27 | import "github.com/Goldziher/go-utils/sliceutils" 28 | 29 | // Functional patterns 30 | numbers := []int{1, 2, 3, 4, 5} 31 | doubled := sliceutils.Map(numbers, func(v, i int, s []int) int { return v * 2 }) 32 | evens := sliceutils.Filter(numbers, func(v, i int, s []int) bool { return v%2 == 0 }) 33 | sum := sliceutils.Reduce(numbers, func(acc, v, i int, s []int) int { return acc + v }, 0) 34 | 35 | // LINQ-style operations 36 | type User struct { Name string; Age int } 37 | users := []User{{"Alice", 30}, {"Bob", 25}, {"Charlie", 30}} 38 | byAge := sliceutils.GroupBy(users, func(u User) int { return u.Age }) 39 | adults, minors := sliceutils.Partition(users, func(u User) bool { return u.Age >= 18 }) 40 | ``` 41 | 42 | ### maputils 43 | Map transformations and utilities. 44 | 45 | ```go 46 | import "github.com/Goldziher/go-utils/maputils" 47 | 48 | m := map[string]int{"a": 1, "b": 2, "c": 3} 49 | keys := maputils.Keys(m) // []string{"a", "b", "c"} 50 | values := maputils.Values(m) // []int{1, 2, 3} 51 | filtered := maputils.Filter(m, func(k string, v int) bool { return v > 1 }) 52 | ``` 53 | 54 | ### stringutils 55 | String manipulation with type-safe conversion. 56 | 57 | ```go 58 | import "github.com/Goldziher/go-utils/stringutils" 59 | 60 | // Type-safe stringify with options 61 | str := stringutils.Stringify(42, stringutils.Options{Base: 16}) // "2a" 62 | 63 | // String manipulation 64 | padded := stringutils.PadLeft("42", "0", 5) // "00042" 65 | capitalized := stringutils.Capitalize("hello") // "Hello" 66 | ``` 67 | 68 | ### structutils 69 | Reflection-based struct utilities with tag support. 70 | 71 | ```go 72 | import "github.com/Goldziher/go-utils/structutils" 73 | 74 | type Config struct { 75 | Host string `json:"host"` 76 | Port int `json:"port"` 77 | } 78 | 79 | cfg := Config{Host: "localhost", Port: 8080} 80 | m := structutils.ToMap(cfg, "json") // map[string]any{"host": "localhost", "port": 8080} 81 | ``` 82 | 83 | ### dateutils 84 | Time and date utilities for business logic. 85 | 86 | ```go 87 | import "github.com/Goldziher/go-utils/dateutils" 88 | 89 | // Business day calculations 90 | future := dateutils.AddBusinessDays(time.Now(), 5) 91 | 92 | // Date overlap detection 93 | overlaps := dateutils.Overlap(start1, end1, start2, end2) 94 | 95 | // Age calculation 96 | age := dateutils.Age(birthdate) 97 | ``` 98 | 99 | ### urlutils 100 | URL parsing and query string builders. 101 | 102 | ```go 103 | import "github.com/Goldziher/go-utils/urlutils" 104 | 105 | // Query string from map 106 | params := map[string]any{"page": 1, "tags": []string{"go", "utils"}} 107 | query := urlutils.QueryStringifyMap(params) // "page=1&tags=go&tags=utils" 108 | 109 | // Query string from struct with tags 110 | type Query struct { 111 | Page int `qs:"page"` 112 | Tags []string `qs:"tag"` 113 | } 114 | q := Query{Page: 1, Tags: []string{"go", "utils"}} 115 | query := urlutils.QueryStringifyStruct(q, "qs") 116 | ``` 117 | 118 | ### mathutils 119 | Generic math operations with type constraints. 120 | 121 | ```go 122 | import "github.com/Goldziher/go-utils/mathutils" 123 | 124 | clamped := mathutils.Clamp(value, min, max) 125 | inRange := mathutils.InRange(value, min, max) 126 | isPrime := mathutils.IsPrime(17) // true 127 | gcd := mathutils.Gcd(48, 18) // 6 128 | ``` 129 | 130 | ## Links 131 | 132 | - [GitHub Repository](https://github.com/Goldziher/go-utils) 133 | - [Go Package Documentation](https://pkg.go.dev/github.com/Goldziher/go-utils) 134 | - [Issue Tracker](https://github.com/Goldziher/go-utils/issues) 135 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | run: 4 | timeout: 5m 5 | issues-exit-code: 1 6 | tests: true 7 | concurrency: 4 8 | modules-download-mode: readonly 9 | allow-serial-runners: false 10 | allow-parallel-runners: true 11 | 12 | linters: 13 | default: none 14 | enable: 15 | - errcheck 16 | - govet 17 | - ineffassign 18 | - staticcheck 19 | - unused 20 | - revive 21 | - gocyclo 22 | - goconst 23 | - gocritic 24 | - gosec 25 | - misspell 26 | - nakedret 27 | settings: 28 | errcheck: 29 | check-type-assertions: true 30 | check-blank: true 31 | exclude-functions: 32 | - (net/http.ResponseWriter).Write 33 | - (io.Closer).Close 34 | - fmt.Fprintf 35 | - fmt.Printf 36 | - fmt.Println 37 | - os.Setenv 38 | - os.Unsetenv 39 | goconst: 40 | min-len: 3 41 | min-occurrences: 3 42 | gocyclo: 43 | min-complexity: 25 44 | gosec: 45 | excludes: 46 | - G101 # ~keep hardcoded credentials check (too many false positives) 47 | govet: 48 | enable-all: true 49 | disable: 50 | - shadow 51 | misspell: 52 | locale: US 53 | nakedret: 54 | max-func-lines: 30 55 | revive: 56 | confidence: 0.8 57 | severity: warning 58 | enable-all-rules: false 59 | rules: 60 | - name: blank-imports 61 | - name: context-keys-type 62 | - name: time-naming 63 | - name: var-declaration 64 | - name: unexported-return 65 | - name: errorf 66 | - name: context-as-argument 67 | - name: dot-imports 68 | - name: error-return 69 | - name: error-strings 70 | - name: error-naming 71 | - name: if-return 72 | - name: increment-decrement 73 | - name: var-naming 74 | - name: range 75 | - name: receiver-naming 76 | - name: indent-error-flow 77 | - name: exported 78 | disabled: true 79 | - name: package-comments 80 | disabled: true 81 | exclusions: 82 | generated: lax 83 | rules: 84 | # Test files: More lenient for repeated strings 85 | - linters: 86 | - goconst 87 | path: _test\.go 88 | # Test files: More lenient for cyclomatic complexity 89 | - linters: 90 | - gocyclo 91 | path: _test\.go 92 | # Test files: Skip all gosec security checks (tests don't need production security standards) 93 | - linters: 94 | - gosec 95 | path: _test\.go 96 | # Test files: Allow flexible context parameter ordering in test helpers 97 | - linters: 98 | - revive 99 | path: _test\.go 100 | text: "context-as-argument" 101 | # Test files: Allow unused writes (test setup artifacts) 102 | - linters: 103 | - goconst 104 | - revive 105 | - errcheck 106 | - govet 107 | path: _test\.go 108 | text: "unusedwrite:" 109 | # All files: Field alignment is not critical 110 | - linters: 111 | - govet 112 | text: "fieldalignment:" 113 | # Test files: More lenient for error checking (test assertions handle errors differently) 114 | - linters: 115 | - errcheck 116 | path: _test\.go 117 | text: "Error return value.*is not checked" 118 | paths: 119 | - vendor 120 | - build 121 | - deployments 122 | - third_party$ 123 | - builtin$ 124 | - examples$ 125 | 126 | issues: 127 | max-issues-per-linter: 0 128 | max-same-issues: 0 129 | uniq-by-line: true 130 | new: false 131 | exclude: 132 | - "Error return value of `\\(\\*github\\.com/goccy/go-json\\.Encoder\\)\\.Encode` is not checked" 133 | - "Error return value of `w\\.Write` is not checked" 134 | - "Error return value of `resp\\.Body\\.Close` is not checked" 135 | - "Error return value of `res\\.Body\\.Close` is not checked" 136 | - "Error return value of `r\\.Body\\.Read` is not checked" 137 | - "Error return value of `os\\.Setenv` is not checked" 138 | - "Error return value of `os\\.Unsetenv` is not checked" 139 | - 'shadow: declaration of "err" shadows declaration' 140 | - "unusedwrite: unused write to field" 141 | - "Error return value of `c\\.provider\\.Delete` is not checked" 142 | - "Error return value of `provider\\.Close` is not checked" 143 | - "Error return value of `natsClient\\.Close` is not checked" 144 | - "Error return value of `cacheProvider\\.Close` is not checked" 145 | - "Error return value of `processor\\.Close` is not checked" 146 | - "Error return value of `sub\\.Unsubscribe` is not checked" 147 | - "Error return value of `json\\.Marshal` is not checked" 148 | - "Error return value of `strconv\\." 149 | - "Error return value of `fmt\\.Sscanf` is not checked" 150 | - "Error return value is not checked" 151 | 152 | formatters: 153 | exclusions: 154 | generated: lax 155 | paths: 156 | - third_party$ 157 | - builtin$ 158 | - examples$ 159 | -------------------------------------------------------------------------------- /urlutils/urlutils_test.go: -------------------------------------------------------------------------------- 1 | package urlutils_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/Goldziher/go-utils/urlutils" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | var nilSlice []int 12 | 13 | var nilMap map[string]any 14 | 15 | func TestQueryStringifyMap(t *testing.T) { 16 | t.Run("Test nil map", func(t *testing.T) { 17 | actualOutput := urlutils.QueryStringifyMap(nilMap) 18 | assert.Equal(t, "", actualOutput) 19 | }) 20 | t.Run("Test string any map", func(t *testing.T) { 21 | actualOutput := urlutils.QueryStringifyMap(map[string]any{ 22 | "user": "moishe", 23 | "active": true, 24 | "age": 100, 25 | "friends": []int{1, 2, 3, 4, 5, 6}, 26 | }) 27 | assert.Equal( 28 | t, 29 | "active=true&age=100&friends=1&friends=2&friends=3&friends=4&friends=5&friends=6&user=moishe", 30 | actualOutput, 31 | ) 32 | }) 33 | t.Run("Test string any map with nil slice", func(t *testing.T) { 34 | actualOutput := urlutils.QueryStringifyMap(map[string]any{ 35 | "user": "moishe", 36 | "active": true, 37 | "age": 100, 38 | "friends": nilSlice, 39 | }) 40 | assert.Equal(t, "active=true&age=100&friends=&user=moishe", actualOutput) 41 | }) 42 | t.Run("Test string int map", func(t *testing.T) { 43 | actualOutput := urlutils.QueryStringifyMap(map[string]int{ 44 | "user": 1, 45 | }) 46 | assert.Equal(t, "user=1", actualOutput) 47 | }) 48 | t.Run("Test int int map", func(t *testing.T) { 49 | actualOutput := urlutils.QueryStringifyMap(map[int]int{ 50 | 1: 2, 51 | }) 52 | assert.Equal(t, "1=2", actualOutput) 53 | }) 54 | } 55 | 56 | func TestQueryStringifyStruct(t *testing.T) { 57 | testCases := []struct { 58 | input any 59 | expectedOutput string 60 | structTag string 61 | }{ 62 | { 63 | struct { 64 | User string 65 | Active bool 66 | Age int 67 | Friends []int 68 | }{ 69 | User: "moishe", 70 | Active: true, 71 | Age: 100, 72 | Friends: []int{1, 2, 3, 4, 5, 6}, 73 | }, 74 | "Active=true&Age=100&Friends=1&Friends=2&Friends=3&Friends=4&Friends=5&Friends=6&User=moishe", 75 | "", 76 | }, 77 | { 78 | struct { 79 | User string 80 | Active bool 81 | Age int 82 | Friends []int 83 | }{ 84 | User: "moishe", 85 | Active: true, 86 | Age: 100, 87 | Friends: nilSlice, 88 | }, 89 | "Active=true&Age=100&Friends=&User=moishe", 90 | "", 91 | }, 92 | { 93 | struct { 94 | User string 95 | Active bool 96 | Age int `qs:"-"` 97 | Friends []int 98 | }{ 99 | User: "moishe", 100 | Active: true, 101 | Age: 100, 102 | Friends: []int{1, 2, 3, 4, 5, 6}, 103 | }, 104 | "Active=true&Friends=1&Friends=2&Friends=3&Friends=4&Friends=5&Friends=6&User=moishe", 105 | "qs", 106 | }, 107 | { 108 | struct { 109 | User string 110 | Active bool 111 | Age int `qs:"age"` 112 | Friends []int 113 | }{ 114 | User: "moishe", 115 | Active: true, 116 | Age: 100, 117 | Friends: []int{1, 2, 3, 4, 5, 6}, 118 | }, 119 | "Active=true&Friends=1&Friends=2&Friends=3&Friends=4&Friends=5&Friends=6&User=moishe&age=100", 120 | "qs", 121 | }, 122 | } 123 | 124 | for _, testCase := range testCases { 125 | t.Run(fmt.Sprintf("Test: %s", testCase.expectedOutput), func(t *testing.T) { 126 | assert.Equal( 127 | t, 128 | testCase.expectedOutput, 129 | urlutils.QueryStringifyStruct(testCase.input, testCase.structTag), 130 | ) 131 | }) 132 | } 133 | } 134 | 135 | func TestParse(t *testing.T) { 136 | u, err := urlutils.Parse("https://example.com/path?key=value") 137 | assert.NoError(t, err) 138 | assert.NotNil(t, u) 139 | assert.Equal(t, "https", u.Scheme) 140 | assert.Equal(t, "example.com", u.Host) 141 | assert.Equal(t, "/path", u.Path) 142 | 143 | _, err = urlutils.Parse("://invalid") 144 | assert.Error(t, err) 145 | } 146 | 147 | func TestMustParse(t *testing.T) { 148 | u := urlutils.MustParse("https://example.com/path") 149 | assert.NotNil(t, u) 150 | assert.Equal(t, "https", u.Scheme) 151 | 152 | assert.Panics(t, func() { 153 | urlutils.MustParse("://invalid") 154 | }) 155 | } 156 | 157 | func TestIsAbsolute(t *testing.T) { 158 | assert.True(t, urlutils.IsAbsolute("https://example.com")) 159 | assert.True(t, urlutils.IsAbsolute("http://example.com/path")) 160 | assert.False(t, urlutils.IsAbsolute("/path")) 161 | assert.False(t, urlutils.IsAbsolute("path")) 162 | assert.False(t, urlutils.IsAbsolute("://invalid")) 163 | } 164 | 165 | func TestGetDomain(t *testing.T) { 166 | assert.Equal(t, "example.com", urlutils.GetDomain("https://example.com/path")) 167 | assert.Equal(t, "example.com:8080", urlutils.GetDomain("https://example.com:8080/path")) 168 | assert.Equal(t, "", urlutils.GetDomain("/path")) 169 | assert.Equal(t, "", urlutils.GetDomain("://invalid")) 170 | } 171 | 172 | func TestGetScheme(t *testing.T) { 173 | assert.Equal(t, "https", urlutils.GetScheme("https://example.com")) 174 | assert.Equal(t, "http", urlutils.GetScheme("http://example.com")) 175 | assert.Equal(t, "", urlutils.GetScheme("/path")) 176 | assert.Equal(t, "", urlutils.GetScheme("://invalid")) 177 | } 178 | 179 | func TestGetPath(t *testing.T) { 180 | assert.Equal(t, "/path/to/resource", urlutils.GetPath("https://example.com/path/to/resource")) 181 | assert.Equal(t, "/", urlutils.GetPath("https://example.com/")) 182 | assert.Equal(t, "", urlutils.GetPath("https://example.com")) 183 | assert.Equal(t, "", urlutils.GetPath("://invalid")) 184 | } 185 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Utils 2 | 3 |
4 | 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/Goldziher/go-utils)](https://goreportcard.com/report/github.com/Goldziher/go-utils) 6 | [![Go Reference](https://pkg.go.dev/badge/github.com/Goldziher/go-utils.svg)](https://pkg.go.dev/github.com/Goldziher/go-utils) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 8 | 9 |
10 | 11 | Functional programming utilities for **Go 1.21+** designed to complement stdlib's `slices`, `maps`, and `cmp` packages. 12 | 13 | ## Why Go Utils? 14 | 15 | **Complementary to stdlib**: Use `slices.Index`, `slices.Contains`, `slices.Clone` for basic operations. Use go-utils for functional patterns (Map, Filter, Reduce), LINQ-style operations (GroupBy, Partition), and utilities not in stdlib. 16 | 17 | **Type-safe with generics**: All functions use Go 1.21+ generics with appropriate constraints for compile-time type safety. 18 | 19 | **Immutable by default**: Functions don't mutate inputs. Operations return new values. 20 | 21 | **100% test coverage**: Every function is comprehensively tested. 22 | 23 | ## Installation 24 | 25 | ```bash 26 | go get -u github.com/Goldziher/go-utils 27 | ``` 28 | 29 | ## Quick Examples 30 | 31 | ### Functional Slice Operations 32 | 33 | ```go 34 | import "github.com/Goldziher/go-utils/sliceutils" 35 | 36 | numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 37 | 38 | // Chain functional operations 39 | evens := sliceutils.Filter(numbers, func(v, i int, s []int) bool { 40 | return v%2 == 0 41 | }) 42 | 43 | doubled := sliceutils.Map(evens, func(v, i int, s []int) int { 44 | return v * 2 45 | }) 46 | 47 | sum := sliceutils.Reduce(doubled, func(acc, v, i int, s []int) int { 48 | return acc + v 49 | }, 0) 50 | // Result: sum = 60 (2+4+6+8+10 doubled = 4+8+12+16+20 = 60) 51 | ``` 52 | 53 | ### LINQ-Style Operations 54 | 55 | ```go 56 | type User struct { 57 | Name string 58 | Age int 59 | Role string 60 | } 61 | 62 | users := []User{ 63 | {"Alice", 30, "admin"}, 64 | {"Bob", 25, "user"}, 65 | {"Charlie", 30, "user"}, 66 | } 67 | 68 | // Group by age 69 | byAge := sliceutils.GroupBy(users, func(u User) int { return u.Age }) 70 | // Result: map[int][]User{25: [{Bob 25 user}], 30: [{Alice 30 admin}, {Charlie 30 user}]} 71 | 72 | // Partition by predicate 73 | admins, regularUsers := sliceutils.Partition(users, func(u User) bool { 74 | return u.Role == "admin" 75 | }) 76 | 77 | // Get distinct ages 78 | ages := sliceutils.DistinctBy(users, func(u User) int { return u.Age }) 79 | ``` 80 | 81 | ### Map Transformations 82 | 83 | ```go 84 | import "github.com/Goldziher/go-utils/maputils" 85 | 86 | data := map[string]int{"apple": 5, "banana": 3, "cherry": 8} 87 | 88 | // Extract keys and values 89 | keys := maputils.Keys(data) // []string{"apple", "banana", "cherry"} 90 | values := maputils.Values(data) // []int{5, 3, 8} 91 | 92 | // Filter map entries 93 | filtered := maputils.Filter(data, func(k string, v int) bool { 94 | return v > 4 95 | }) 96 | // Result: map[string]int{"apple": 5, "cherry": 8} 97 | ``` 98 | 99 | ### Type-Safe String Conversion 100 | 101 | ```go 102 | import "github.com/Goldziher/go-utils/stringutils" 103 | 104 | // Stringify any type with options 105 | hex := stringutils.Stringify(42, stringutils.Options{Base: 16}) // "2a" 106 | float := stringutils.Stringify(3.14159, stringutils.Options{Precision: 2}) // "3.14" 107 | 108 | // Stringify complex types 109 | m := map[string]int{"a": 1, "b": 2} 110 | str := stringutils.Stringify(m) // "{a: 1, b: 2}" 111 | ``` 112 | 113 | ### Business Date Calculations 114 | 115 | ```go 116 | import "github.com/Goldziher/go-utils/dateutils" 117 | 118 | // Add business days (skips weekends) 119 | future := dateutils.AddBusinessDays(time.Now(), 5) 120 | 121 | // Check date range overlap 122 | overlaps := dateutils.Overlap(start1, end1, start2, end2) 123 | 124 | // Calculate age 125 | age := dateutils.Age(birthdate) 126 | ``` 127 | 128 | ## Packages 129 | 130 | | Package | Description | Key Functions | 131 | |---------|-------------|---------------| 132 | | **[sliceutils](https://pkg.go.dev/github.com/Goldziher/go-utils/sliceutils)** | Functional and LINQ-style slice operations | Map, Filter, Reduce, GroupBy, Partition, Find | 133 | | **[maputils](https://pkg.go.dev/github.com/Goldziher/go-utils/maputils)** | Map transformations | Keys, Values, Filter, Merge, Invert | 134 | | **[stringutils](https://pkg.go.dev/github.com/Goldziher/go-utils/stringutils)** | String manipulation | Stringify, ToCamelCase, ToSnakeCase, Truncate | 135 | | **[structutils](https://pkg.go.dev/github.com/Goldziher/go-utils/structutils)** | Struct reflection utilities | ToMap, ForEach, FieldNames (tag-aware) | 136 | | **[dateutils](https://pkg.go.dev/github.com/Goldziher/go-utils/dateutils)** | Date/time utilities | AddBusinessDays, Overlap, Age, StartOfWeek | 137 | | **[urlutils](https://pkg.go.dev/github.com/Goldziher/go-utils/urlutils)** | URL and query string builders | QueryStringifyMap, QueryStringifyStruct | 138 | | **[mathutils](https://pkg.go.dev/github.com/Goldziher/go-utils/mathutils)** | Generic math operations | Clamp, InRange, Gcd, Lcm, IsPrime | 139 | | **[ptrutils](https://pkg.go.dev/github.com/Goldziher/go-utils/ptrutils)** | Pointer utilities | ToPtr, Deref | 140 | | **[excutils](https://pkg.go.dev/github.com/Goldziher/go-utils/excutils)** | Exception-style error handling | Panic, Try, Must | 141 | 142 | ## Documentation 143 | 144 | Full documentation available at [goldziher.github.io/go-utils](https://goldziher.github.io/go-utils/) 145 | 146 | API reference at [pkg.go.dev/github.com/Goldziher/go-utils](https://pkg.go.dev/github.com/Goldziher/go-utils) 147 | 148 | ## Contributing 149 | 150 | Contributions are welcome! Please read the [Contributing Guide](CONTRIBUTING.md) before submitting PRs. 151 | 152 | ## License 153 | 154 | MIT License - see [LICENSE](LICENSE) for details. 155 | 156 | ## Author 157 | 158 | [Na'aman Hirschfeld](https://github.com/Goldziher) 159 | -------------------------------------------------------------------------------- /excutils/excutils.go: -------------------------------------------------------------------------------- 1 | // This package includes utility functions for error handling and exceptions. 2 | // It provides helpers for common error handling patterns including panic-on-error, 3 | // error recovery, and safe error checking. 4 | 5 | package exc 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | // MustResult panics if the error is not nil, otherwise returns the result. 13 | // This is useful for cases where an error should never occur and you want to fail fast. 14 | // The optional messages are formatted and included in the panic message. 15 | func MustResult[T any](value T, err error, messages ...string) T { 16 | if err != nil { 17 | msg := formatMessages(messages) 18 | if msg != "" { 19 | panic(fmt.Errorf("%s: %w", msg, err)) 20 | } 21 | panic(err) 22 | } 23 | return value 24 | } 25 | 26 | // Must panics if the error is not nil. 27 | // This is useful for initialization or setup code where errors should never occur. 28 | // The optional messages are formatted and included in the panic message. 29 | func Must(err error, messages ...string) { 30 | if err != nil { 31 | msg := formatMessages(messages) 32 | if msg != "" { 33 | panic(fmt.Errorf("%s: %w", msg, err)) 34 | } 35 | panic(err) 36 | } 37 | } 38 | 39 | // Try executes a function and returns its error. 40 | // This is useful for defer statements or cleanup operations where you want to capture errors. 41 | func Try(fn func() error) (err error) { 42 | defer func() { 43 | if r := recover(); r != nil { 44 | if e, ok := r.(error); ok { 45 | err = e 46 | } else { 47 | err = fmt.Errorf("panic: %v", r) 48 | } 49 | } 50 | }() 51 | return fn() 52 | } 53 | 54 | // Catch executes a function and recovers from any panics, converting them to errors. 55 | // Returns the function's error if any, or an error created from a recovered panic. 56 | func Catch(fn func() error) (err error) { 57 | defer func() { 58 | if r := recover(); r != nil { 59 | if e, ok := r.(error); ok { 60 | err = e 61 | } else { 62 | err = fmt.Errorf("panic: %v", r) 63 | } 64 | } 65 | }() 66 | return fn() 67 | } 68 | 69 | // IgnoreErr executes a function that returns an error and ignores the error. 70 | // This is useful for cleanup operations where errors can be safely ignored. 71 | func IgnoreErr(fn func() error) { 72 | _ = fn() //nolint:errcheck // explicitly ignoring error by design 73 | } 74 | 75 | // FirstErr returns the first non-nil error from a list of errors. 76 | // Returns nil if all errors are nil. 77 | func FirstErr(errs ...error) error { 78 | for _, err := range errs { 79 | if err != nil { 80 | return err 81 | } 82 | } 83 | return nil 84 | } 85 | 86 | // ReturnAnyErr returns the first error found in the list of values, if any. 87 | // This is useful when dealing with variadic functions that may return errors. 88 | func ReturnAnyErr(values ...any) error { 89 | for _, value := range values { 90 | if err, ok := value.(error); ok && err != nil { 91 | return err 92 | } 93 | } 94 | return nil 95 | } 96 | 97 | // ReturnNotNil panics if the value is nil, otherwise returns the value. 98 | // This is useful for asserting that a pointer must not be nil. 99 | // The optional messages are formatted and included in the panic message. 100 | func ReturnNotNil[T any](value *T, messages ...string) *T { 101 | if value == nil { 102 | msg := formatMessages(messages) 103 | if msg == "" { 104 | msg = "unexpected nil value" 105 | } 106 | panic(errors.New(msg)) 107 | } 108 | return value 109 | } 110 | 111 | // MustNotNil panics with a formatted error message if the value is nil. 112 | // Returns the dereferenced value if not nil. 113 | func MustNotNil[T any](value *T, messages ...string) T { 114 | if value == nil { 115 | msg := formatMessages(messages) 116 | if msg == "" { 117 | msg = "unexpected nil value" 118 | } 119 | panic(errors.New(msg)) 120 | } 121 | return *value 122 | } 123 | 124 | // AllErr returns an error containing all non-nil errors from the list. 125 | // Returns nil if all errors are nil. 126 | // Multiple errors are joined using errors.Join (Go 1.20+). 127 | func AllErr(errs ...error) error { 128 | var nonNilErrs []error 129 | for _, err := range errs { 130 | if err != nil { 131 | nonNilErrs = append(nonNilErrs, err) 132 | } 133 | } 134 | if len(nonNilErrs) == 0 { 135 | return nil 136 | } 137 | return errors.Join(nonNilErrs...) 138 | } 139 | 140 | // formatMessages joins multiple messages into a single string. 141 | func formatMessages(messages []string) string { 142 | if len(messages) == 0 { 143 | return "" 144 | } 145 | result := messages[0] 146 | for i := 1; i < len(messages); i++ { 147 | if messages[i] != "" { 148 | result += " " + messages[i] 149 | } 150 | } 151 | return result 152 | } 153 | 154 | // Retry executes a function up to maxAttempts times, returning the first successful result. 155 | // Returns the last error if all attempts fail. 156 | // Returns an error if maxAttempts is less than 1. 157 | func Retry(fn func() error, maxAttempts int) error { 158 | if maxAttempts < 1 { 159 | return errors.New("maxAttempts must be at least 1") 160 | } 161 | var lastErr error 162 | for i := 0; i < maxAttempts; i++ { 163 | lastErr = fn() 164 | if lastErr == nil { 165 | return nil 166 | } 167 | } 168 | return lastErr 169 | } 170 | 171 | // RetryWithResult executes a function up to maxAttempts times, returning the first successful result. 172 | // Returns the result and error from the last attempt if all fail. 173 | func RetryWithResult[T any](fn func() (T, error), maxAttempts int) (T, error) { 174 | var result T 175 | var lastErr error 176 | for i := 0; i < maxAttempts; i++ { 177 | result, lastErr = fn() 178 | if lastErr == nil { 179 | return result, nil 180 | } 181 | } 182 | return result, lastErr 183 | } 184 | 185 | // RecoverWithValue recovers from a panic and returns the specified default value. 186 | func RecoverWithValue[T any](fn func() T, defaultValue T) (result T) { 187 | defer func() { 188 | if r := recover(); r != nil { 189 | result = defaultValue 190 | } 191 | }() 192 | result = fn() 193 | return result 194 | } 195 | -------------------------------------------------------------------------------- /maputils/maputils_test.go: -------------------------------------------------------------------------------- 1 | package maputils_test 2 | 3 | import ( 4 | "sort" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/Goldziher/go-utils/maputils" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestKeys(t *testing.T) { 13 | var daysMap = map[string]int{ 14 | "Sunday": 1, 15 | "Monday": 2, 16 | "Tuesday": 3, 17 | "Wednesday": 4, 18 | "Thursday": 5, 19 | "Friday": 6, 20 | "Saturday": 7, 21 | } 22 | keys := maputils.Keys(daysMap) 23 | days := []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} 24 | sort.Strings(days) 25 | sort.Strings(keys) 26 | assert.Equal(t, days, keys) 27 | } 28 | 29 | func TestValues(t *testing.T) { 30 | var daysMap = map[string]int{ 31 | "Sunday": 1, 32 | "Monday": 2, 33 | "Tuesday": 3, 34 | "Wednesday": 4, 35 | "Thursday": 5, 36 | "Friday": 6, 37 | "Saturday": 7, 38 | } 39 | values := maputils.Values(daysMap) 40 | sort.Ints(values) 41 | assert.Equal(t, []int{1, 2, 3, 4, 5, 6, 7}, values) 42 | } 43 | 44 | func TestMerge(t *testing.T) { 45 | first := map[string]string{ 46 | "George": "Harrison", 47 | "Betty": "Davis", 48 | } 49 | second := map[string]string{ 50 | "Ronald": "Reagen", 51 | "Betty": "Stewart", 52 | } 53 | 54 | assert.Equal(t, 55 | map[string]string{ 56 | "George": "Harrison", 57 | "Betty": "Stewart", 58 | "Ronald": "Reagen", 59 | }, maputils.Merge(first, second)) 60 | } 61 | 62 | func TestForEach(t *testing.T) { 63 | var daysMap = map[string]int{ 64 | "Sunday": 1, 65 | "Monday": 2, 66 | "Tuesday": 3, 67 | "Wednesday": 4, 68 | "Thursday": 5, 69 | "Friday": 6, 70 | "Saturday": 7, 71 | } 72 | 73 | sum := 0 74 | 75 | maputils.ForEach(daysMap, func(_ string, day int) { 76 | sum += day 77 | }) 78 | 79 | assert.Equal(t, 28, sum) 80 | } 81 | 82 | func TestDrop(t *testing.T) { 83 | var daysMap = map[string]int{ 84 | "Sunday": 1, 85 | "Monday": 2, 86 | "Tuesday": 3, 87 | "Wednesday": 4, 88 | "Thursday": 5, 89 | "Friday": 6, 90 | "Saturday": 7, 91 | } 92 | 93 | expectedResult := map[string]int{ 94 | "Sunday": 1, 95 | "Friday": 6, 96 | } 97 | assert.Equal( 98 | t, 99 | expectedResult, 100 | maputils.Drop(daysMap, []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Saturday"}), 101 | ) 102 | // ensure we do not modify the original value 103 | assert.Equal(t, expectedResult, daysMap) 104 | } 105 | 106 | func TestCopy(t *testing.T) { 107 | var daysMap = map[string]int{ 108 | "Sunday": 1, 109 | "Monday": 2, 110 | "Tuesday": 3, 111 | "Wednesday": 4, 112 | "Thursday": 5, 113 | "Friday": 6, 114 | "Saturday": 7, 115 | } 116 | daysCopy := maputils.Copy(daysMap) 117 | assert.Equal(t, daysMap, daysCopy) 118 | delete(daysCopy, "Sunday") 119 | assert.NotEqual(t, daysMap, daysCopy) 120 | } 121 | 122 | func TestFilter(t *testing.T) { 123 | var daysMap = map[string]int{ 124 | "Sunday": 1, 125 | "Monday": 2, 126 | "Tuesday": 3, 127 | "Wednesday": 4, 128 | "Thursday": 5, 129 | "Friday": 6, 130 | "Saturday": 7, 131 | } 132 | 133 | var expectedResult = map[string]int{ 134 | "Monday": 2, 135 | "Wednesday": 4, 136 | "Friday": 6, 137 | } 138 | 139 | filteredDays := maputils.Filter(daysMap, func(_ string, value int) bool { 140 | return value%2 == 0 141 | }) 142 | 143 | assert.Equal(t, expectedResult, filteredDays) 144 | } 145 | 146 | func TestMap(t *testing.T) { 147 | input := map[string]int{"a": 1, "b": 2, "c": 3} 148 | 149 | result := maputils.Map(input, func(k string, v int) string { 150 | return k + strconv.Itoa(v*2) 151 | }) 152 | 153 | assert.Equal(t, "a2", result["a"]) 154 | assert.Equal(t, "b4", result["b"]) 155 | assert.Equal(t, "c6", result["c"]) 156 | } 157 | 158 | func TestMapKeys(t *testing.T) { 159 | input := map[string]int{"a": 1, "b": 2, "c": 3} 160 | 161 | result := maputils.MapKeys(input, func(k string, v int) int { 162 | return v * 10 163 | }) 164 | 165 | assert.Equal(t, 1, result[10]) 166 | assert.Equal(t, 2, result[20]) 167 | assert.Equal(t, 3, result[30]) 168 | } 169 | 170 | func TestInvert(t *testing.T) { 171 | input := map[string]int{"a": 1, "b": 2, "c": 3} 172 | 173 | result := maputils.Invert(input) 174 | 175 | assert.Equal(t, "a", result[1]) 176 | assert.Equal(t, "b", result[2]) 177 | assert.Equal(t, "c", result[3]) 178 | assert.Len(t, result, 3) 179 | } 180 | 181 | func TestPick(t *testing.T) { 182 | input := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4} 183 | 184 | result := maputils.Pick(input, []string{"a", "c", "e"}) 185 | 186 | assert.Equal(t, 1, result["a"]) 187 | assert.Equal(t, 3, result["c"]) 188 | assert.NotContains(t, result, "b") 189 | assert.NotContains(t, result, "d") 190 | assert.NotContains(t, result, "e") 191 | assert.Len(t, result, 2) 192 | } 193 | 194 | func TestOmit(t *testing.T) { 195 | input := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4} 196 | 197 | result := maputils.Omit(input, []string{"b", "d"}) 198 | 199 | assert.Equal(t, 1, result["a"]) 200 | assert.Equal(t, 3, result["c"]) 201 | assert.NotContains(t, result, "b") 202 | assert.NotContains(t, result, "d") 203 | assert.Len(t, result, 2) 204 | } 205 | 206 | func TestHas(t *testing.T) { 207 | input := map[string]int{"a": 1, "b": 2} 208 | 209 | assert.True(t, maputils.Has(input, "a")) 210 | assert.True(t, maputils.Has(input, "b")) 211 | assert.False(t, maputils.Has(input, "c")) 212 | } 213 | 214 | func TestGet(t *testing.T) { 215 | input := map[string]int{"a": 1, "b": 2} 216 | 217 | assert.Equal(t, 1, maputils.Get(input, "a", 0)) 218 | assert.Equal(t, 2, maputils.Get(input, "b", 0)) 219 | assert.Equal(t, 99, maputils.Get(input, "c", 99)) 220 | } 221 | 222 | func TestToEntries(t *testing.T) { 223 | input := map[string]int{"a": 1, "b": 2} 224 | 225 | result := maputils.ToEntries(input) 226 | 227 | assert.Len(t, result, 2) 228 | 229 | // Check that entries exist (order is non-deterministic) 230 | hasA := false 231 | hasB := false 232 | for _, entry := range result { 233 | if entry[0] == "a" && entry[1] == 1 { 234 | hasA = true 235 | } 236 | if entry[0] == "b" && entry[1] == 2 { 237 | hasB = true 238 | } 239 | } 240 | assert.True(t, hasA) 241 | assert.True(t, hasB) 242 | } 243 | 244 | func TestFromEntries(t *testing.T) { 245 | entries := [][2]any{ 246 | {"a", 1}, 247 | {"b", 2}, 248 | {"c", 3}, 249 | } 250 | 251 | result := maputils.FromEntries[string, int](entries) 252 | 253 | assert.Equal(t, 1, result["a"]) 254 | assert.Equal(t, 2, result["b"]) 255 | assert.Equal(t, 3, result["c"]) 256 | assert.Len(t, result, 3) 257 | } 258 | 259 | func TestMapGroupBy(t *testing.T) { 260 | input := map[string]int{ 261 | "apple": 10, 262 | "banana": 5, 263 | "cherry": 15, 264 | "date": 5, 265 | } 266 | 267 | result := maputils.GroupBy(input, func(k string, v int) string { 268 | if v < 10 { 269 | return "small" 270 | } 271 | return "large" 272 | }) 273 | 274 | assert.Len(t, result, 2) 275 | assert.Len(t, result["small"], 2) 276 | assert.Len(t, result["large"], 2) 277 | assert.Equal(t, 5, result["small"]["banana"]) 278 | assert.Equal(t, 5, result["small"]["date"]) 279 | assert.Equal(t, 10, result["large"]["apple"]) 280 | assert.Equal(t, 15, result["large"]["cherry"]) 281 | } 282 | -------------------------------------------------------------------------------- /maputils/maputils.go: -------------------------------------------------------------------------------- 1 | // This package includes utility functions for handling and manipulating maputils. 2 | // It draws inspiration from JavaScript and Python and uses Go generics as a basis. 3 | 4 | package maputils 5 | 6 | // Keys - takes a map with keys K and values V, returns a slice of type K of the map's keys. 7 | // Note: Go maps do not preserve insertion order. 8 | func Keys[K comparable, V any](mapInstance map[K]V) []K { 9 | keys := make([]K, len(mapInstance)) 10 | 11 | i := 0 12 | for k := range mapInstance { 13 | keys[i] = k 14 | i++ 15 | } 16 | 17 | return keys 18 | } 19 | 20 | // Values - takes a map with keys K and values V, returns a slice of type V of the map's values. 21 | // Note: Go maps do not preserve insertion order. 22 | func Values[K comparable, V any](mapInstance map[K]V) []V { 23 | values := make([]V, len(mapInstance)) 24 | 25 | i := 0 26 | for _, v := range mapInstance { 27 | values[i] = v 28 | i++ 29 | } 30 | 31 | return values 32 | } 33 | 34 | // Merge - takes an arbitrary number of map instances with keys K and values V and merges them into a single map. 35 | // Note: merge works from left to right. If a key already exists in a previous map, its value is over-written. 36 | func Merge[K comparable, V any](mapInstances ...map[K]V) map[K]V { 37 | var mergedMapSize int 38 | 39 | for _, mapInstance := range mapInstances { 40 | mergedMapSize += len(mapInstance) 41 | } 42 | 43 | mergedMap := make(map[K]V, mergedMapSize) 44 | 45 | for _, mapInstance := range mapInstances { 46 | for k, v := range mapInstance { 47 | mergedMap[k] = v 48 | } 49 | } 50 | 51 | return mergedMap 52 | } 53 | 54 | // ForEach - given a map with keys K and values V, executes the passed in function for each key-value pair. 55 | func ForEach[K comparable, V any](mapInstance map[K]V, function func(key K, value V)) { 56 | for key, value := range mapInstance { 57 | function(key, value) 58 | } 59 | } 60 | 61 | // Drop - takes a map with keys K and values V, and a slice of keys K, dropping all the key-value pairs that match the keys in the slice. 62 | // Note: this function will modify the passed in map. To get a different object, use the Copy function to pass a copy to this function. 63 | func Drop[K comparable, V any](mapInstance map[K]V, keys []K) map[K]V { 64 | for _, key := range keys { 65 | delete(mapInstance, key) 66 | } 67 | 68 | return mapInstance 69 | } 70 | 71 | // Copy - takes a map with keys K and values V and returns a copy of the map. 72 | // 73 | // Deprecated: Copy is deprecated as of Go 1.21. Use maps.Clone from the standard library instead: 74 | // 75 | // import "maps" 76 | // copied := maps.Clone(original) 77 | // 78 | // This function will be removed in a future major version. 79 | func Copy[K comparable, V any](mapInstance map[K]V) map[K]V { 80 | mapCopy := make(map[K]V, len(mapInstance)) 81 | 82 | for key, value := range mapInstance { 83 | mapCopy[key] = value 84 | } 85 | 86 | return mapCopy 87 | } 88 | 89 | // Filter - takes a map with keys K and values V, and executes the passed in function for each key-value pair. 90 | // If the filter function returns true, the key-value pair will be included in the output, otherwise it is filtered out. 91 | func Filter[K comparable, V any](mapInstance map[K]V, function func(key K, value V) bool) map[K]V { 92 | mapCopy := make(map[K]V, len(mapInstance)) 93 | 94 | for key, value := range mapInstance { 95 | if function(key, value) { 96 | mapCopy[key] = value 97 | } 98 | } 99 | 100 | return mapCopy 101 | } 102 | 103 | // Map transforms map values using a mapper function, returning a new map with transformed values. 104 | func Map[K comparable, V any, R any](mapInstance map[K]V, mapper func(key K, value V) R) map[K]R { 105 | result := make(map[K]R, len(mapInstance)) 106 | 107 | for key, value := range mapInstance { 108 | result[key] = mapper(key, value) 109 | } 110 | 111 | return result 112 | } 113 | 114 | // MapKeys transforms map keys using a mapper function, returning a new map with transformed keys. 115 | // Note: If the mapper produces duplicate keys, later values will overwrite earlier ones. 116 | func MapKeys[K comparable, V any, R comparable]( 117 | mapInstance map[K]V, 118 | mapper func(key K, value V) R, 119 | ) map[R]V { 120 | result := make(map[R]V, len(mapInstance)) 121 | 122 | for key, value := range mapInstance { 123 | newKey := mapper(key, value) 124 | result[newKey] = value 125 | } 126 | 127 | return result 128 | } 129 | 130 | // Invert swaps keys and values in a map. 131 | // Note: If multiple keys have the same value, only one will remain (non-deterministic which one). 132 | // Both keys and values must be comparable types. 133 | func Invert[K, V comparable](mapInstance map[K]V) map[V]K { 134 | result := make(map[V]K, len(mapInstance)) 135 | 136 | for key, value := range mapInstance { 137 | result[value] = key 138 | } 139 | 140 | return result 141 | } 142 | 143 | // Pick creates a new map containing only the specified keys. 144 | // Keys that don't exist in the original map are ignored. 145 | func Pick[K comparable, V any](mapInstance map[K]V, keys []K) map[K]V { 146 | result := make(map[K]V) 147 | 148 | for _, key := range keys { 149 | if value, exists := mapInstance[key]; exists { 150 | result[key] = value 151 | } 152 | } 153 | 154 | return result 155 | } 156 | 157 | // Omit creates a new map excluding the specified keys. 158 | func Omit[K comparable, V any](mapInstance map[K]V, keys []K) map[K]V { 159 | keysToOmit := make(map[K]bool, len(keys)) 160 | for _, key := range keys { 161 | keysToOmit[key] = true 162 | } 163 | 164 | result := make(map[K]V) 165 | for key, value := range mapInstance { 166 | if !keysToOmit[key] { 167 | result[key] = value 168 | } 169 | } 170 | 171 | return result 172 | } 173 | 174 | // Has checks if a key exists in the map. 175 | func Has[K comparable, V any](mapInstance map[K]V, key K) bool { 176 | _, exists := mapInstance[key] 177 | return exists 178 | } 179 | 180 | // Get safely gets a value from the map with a default fallback if the key doesn't exist. 181 | func Get[K comparable, V any](mapInstance map[K]V, key K, defaultValue V) V { 182 | if value, exists := mapInstance[key]; exists { 183 | return value 184 | } 185 | return defaultValue 186 | } 187 | 188 | // ToEntries converts a map to a slice of key-value pairs. 189 | // Note: Order is non-deterministic due to map iteration order. 190 | func ToEntries[K comparable, V any](mapInstance map[K]V) [][2]any { 191 | entries := make([][2]any, 0, len(mapInstance)) 192 | 193 | for key, value := range mapInstance { 194 | entries = append(entries, [2]any{key, value}) 195 | } 196 | 197 | return entries 198 | } 199 | 200 | // FromEntries creates a map from a slice of key-value pairs. 201 | // If duplicate keys exist, later values overwrite earlier ones. 202 | func FromEntries[K comparable, V any](entries [][2]any) map[K]V { 203 | result := make(map[K]V, len(entries)) 204 | 205 | for _, entry := range entries { 206 | if key, ok := entry[0].(K); ok { 207 | if value, ok := entry[1].(V); ok { 208 | result[key] = value 209 | } 210 | } 211 | } 212 | 213 | return result 214 | } 215 | 216 | // GroupBy groups map entries by a grouping key generated from each entry. 217 | // Returns a map where keys are group identifiers and values are maps of entries belonging to that group. 218 | func GroupBy[K comparable, V any, G comparable]( 219 | mapInstance map[K]V, 220 | grouper func(key K, value V) G, 221 | ) map[G]map[K]V { 222 | result := make(map[G]map[K]V) 223 | 224 | for key, value := range mapInstance { 225 | group := grouper(key, value) 226 | if result[group] == nil { 227 | result[group] = make(map[K]V) 228 | } 229 | result[group][key] = value 230 | } 231 | 232 | return result 233 | } 234 | -------------------------------------------------------------------------------- /dateutils/dateutils.go: -------------------------------------------------------------------------------- 1 | package dateutils 2 | 3 | import "time" 4 | 5 | // Floor - takes a datetime and return a datetime from the same day at 00:00:00 (UTC). 6 | func Floor(t time.Time) time.Time { 7 | return t.UTC().Truncate(time.Hour * 24) 8 | } 9 | 10 | // Ceil - takes a datetime and return a datetime from the same day at 23:59:59 (UTC). 11 | func Ceil(t time.Time) time.Time { 12 | // add 24 hours so that we are dealing with tomorrow's datetime 13 | // Floor 14 | // Subtract one second and we have today at 23:59:59 15 | return Floor(t.Add(time.Hour * 24)).Add(time.Second * -1) 16 | } 17 | 18 | // BeforeOrEqual returns true if date is before or equal to milestone. 19 | // 20 | // Deprecated: Use the standard library comparison helpers directly: 21 | // 22 | // !date.After(milestone) 23 | func BeforeOrEqual(milestone time.Time, date time.Time) bool { 24 | return !date.After(milestone) 25 | } 26 | 27 | // AfterOrEqual returns true if date is after or equal to milestone. 28 | // 29 | // Deprecated: Use the standard library comparison helpers directly: 30 | // 31 | // !date.Before(milestone) 32 | func AfterOrEqual(milestone time.Time, date time.Time) bool { 33 | return !date.Before(milestone) 34 | } 35 | 36 | // Overlap - returns true if two date intervals overlap. 37 | func Overlap(start1 time.Time, end1 time.Time, start2 time.Time, end2 time.Time) bool { 38 | beforeOrEqual := func(milestone, date time.Time) bool { 39 | return date.UTC().Before(milestone) || date.UTC().Equal(milestone) 40 | } 41 | afterOrEqual := func(milestone, date time.Time) bool { 42 | return date.UTC().After(milestone) || date.UTC().Equal(milestone) 43 | } 44 | return (afterOrEqual(start2, start1) && beforeOrEqual(end2, start1)) || 45 | (afterOrEqual(start2, end1) && beforeOrEqual(end2, end1)) || 46 | (afterOrEqual(start1, start2) && beforeOrEqual(end1, start2)) || 47 | (afterOrEqual(start1, end2) && beforeOrEqual(end1, end2)) 48 | } 49 | 50 | // GetFirstDayOfMonth returns the first day of the current month at 00:00:00 in the local timezone. 51 | func GetFirstDayOfMonth() time.Time { 52 | now := time.Now() 53 | currentYear, currentMonth, _ := now.Date() 54 | currentLocation := now.Location() 55 | 56 | return time.Date(currentYear, currentMonth, 1, 0, 0, 0, 0, currentLocation) 57 | } 58 | 59 | // GetFirstDayOfMonthFor returns the first day of the month for the given date at 00:00:00 in the date's timezone. 60 | func GetFirstDayOfMonthFor(date time.Time) time.Time { 61 | year, month, _ := date.Date() 62 | location := date.Location() 63 | 64 | return time.Date(year, month, 1, 0, 0, 0, 0, location) 65 | } 66 | 67 | // GetLastDayOfMonth returns the last day of the current month at 23:59:59 in the local timezone. 68 | func GetLastDayOfMonth() time.Time { 69 | firstDay := GetFirstDayOfMonth() 70 | // Add one month and subtract one second 71 | return firstDay.AddDate(0, 1, 0).Add(-time.Second) 72 | } 73 | 74 | // GetLastDayOfMonthFor returns the last day of the month for the given date at 23:59:59 in the date's timezone. 75 | func GetLastDayOfMonthFor(date time.Time) time.Time { 76 | firstDay := GetFirstDayOfMonthFor(date) 77 | // Add one month and subtract one second 78 | return firstDay.AddDate(0, 1, 0).Add(-time.Second) 79 | } 80 | 81 | // ParseDate parses a date string in RFC3339 format. 82 | // If parsing fails, returns the fallback value. 83 | func ParseDate(date string, fallback time.Time) time.Time { 84 | parsedDate, parseErr := time.Parse(time.RFC3339, date) 85 | if parseErr != nil { 86 | return fallback 87 | } 88 | return parsedDate 89 | } 90 | 91 | // ParseDateWithLayout parses a date string using the specified layout. 92 | // If parsing fails, returns the fallback value. 93 | func ParseDateWithLayout(date string, layout string, fallback time.Time) time.Time { 94 | parsedDate, parseErr := time.Parse(layout, date) 95 | if parseErr != nil { 96 | return fallback 97 | } 98 | return parsedDate 99 | } 100 | 101 | // MustParseDate parses a date string in RFC3339 format. 102 | // Panics if parsing fails. 103 | func MustParseDate(date string) time.Time { 104 | parsedDate, parseErr := time.Parse(time.RFC3339, date) 105 | if parseErr != nil { 106 | panic(parseErr) 107 | } 108 | return parsedDate 109 | } 110 | 111 | // MustParseDateWithLayout parses a date string using the specified layout. 112 | // Panics if parsing fails. 113 | func MustParseDateWithLayout(date string, layout string) time.Time { 114 | parsedDate, parseErr := time.Parse(layout, date) 115 | if parseErr != nil { 116 | panic(parseErr) 117 | } 118 | return parsedDate 119 | } 120 | 121 | // StartOfDay returns the start of the day (00:00:00) for the given time in its timezone. 122 | func StartOfDay(t time.Time) time.Time { 123 | year, month, day := t.Date() 124 | return time.Date(year, month, day, 0, 0, 0, 0, t.Location()) 125 | } 126 | 127 | // EndOfDay returns the end of the day (23:59:59.999999999) for the given time in its timezone. 128 | func EndOfDay(t time.Time) time.Time { 129 | year, month, day := t.Date() 130 | return time.Date(year, month, day, 23, 59, 59, 999999999, t.Location()) 131 | } 132 | 133 | // StartOfWeek returns the start of the week (Sunday at 00:00:00) for the given time. 134 | func StartOfWeek(t time.Time) time.Time { 135 | weekday := int(t.Weekday()) 136 | return StartOfDay(t.AddDate(0, 0, -weekday)) 137 | } 138 | 139 | // EndOfWeek returns the end of the week (Saturday at 23:59:59.999999999) for the given time. 140 | func EndOfWeek(t time.Time) time.Time { 141 | weekday := int(t.Weekday()) 142 | daysUntilSaturday := 6 - weekday 143 | return EndOfDay(t.AddDate(0, 0, daysUntilSaturday)) 144 | } 145 | 146 | // DaysBetween returns the number of days between two dates. 147 | // The result is negative if end is before start. 148 | func DaysBetween(start, end time.Time) int { 149 | duration := end.Sub(start) 150 | return int(duration.Hours() / 24) 151 | } 152 | 153 | // IsWeekend returns true if the given time is on a weekend (Saturday or Sunday). 154 | func IsWeekend(t time.Time) bool { 155 | weekday := t.Weekday() 156 | return weekday == time.Saturday || weekday == time.Sunday 157 | } 158 | 159 | // IsWeekday returns true if the given time is on a weekday (Monday through Friday). 160 | func IsWeekday(t time.Time) bool { 161 | return !IsWeekend(t) 162 | } 163 | 164 | // AddBusinessDays adds the specified number of business days to the given time. 165 | // Business days are Monday through Friday, skipping weekends. 166 | // Negative values subtract business days. 167 | func AddBusinessDays(t time.Time, days int) time.Time { 168 | if days == 0 { 169 | return t 170 | } 171 | 172 | direction := 1 173 | if days < 0 { 174 | direction = -1 175 | days = -days 176 | } 177 | 178 | result := t 179 | for i := 0; i < days; { 180 | result = result.AddDate(0, 0, direction) 181 | if IsWeekday(result) { 182 | i++ 183 | } 184 | } 185 | return result 186 | } 187 | 188 | // Age calculates the age in years from the given birthdate to now. 189 | func Age(birthdate time.Time) int { 190 | return AgeAt(birthdate, time.Now()) 191 | } 192 | 193 | // AgeAt calculates the age in years from the given birthdate to the specified date. 194 | func AgeAt(birthdate, at time.Time) int { 195 | age := at.Year() - birthdate.Year() 196 | if at.Month() < birthdate.Month() || 197 | (at.Month() == birthdate.Month() && at.Day() < birthdate.Day()) { 198 | age-- 199 | } 200 | return age 201 | } 202 | 203 | // DaysInMonth returns the number of days in the month of the given time. 204 | func DaysInMonth(t time.Time) int { 205 | // Get the first day of next month, then subtract one day 206 | year, month, _ := t.Date() 207 | firstOfNextMonth := time.Date(year, month+1, 1, 0, 0, 0, 0, t.Location()) 208 | lastOfThisMonth := firstOfNextMonth.AddDate(0, 0, -1) 209 | return lastOfThisMonth.Day() 210 | } 211 | 212 | // IsSameDay returns true if both times are on the same day (same year, month, and day). 213 | func IsSameDay(a, b time.Time) bool { 214 | aYear, aMonth, aDay := a.Date() 215 | bYear, bMonth, bDay := b.Date() 216 | return aYear == bYear && aMonth == bMonth && aDay == bDay 217 | } 218 | 219 | // IsSameMonth returns true if both times are in the same month (same year and month). 220 | func IsSameMonth(a, b time.Time) bool { 221 | aYear, aMonth, _ := a.Date() 222 | bYear, bMonth, _ := b.Date() 223 | return aYear == bYear && aMonth == bMonth 224 | } 225 | -------------------------------------------------------------------------------- /ptrutils/ptrutils_test.go: -------------------------------------------------------------------------------- 1 | package ptr 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestTo(t *testing.T) { 10 | t.Run("should return pointer to int", func(t *testing.T) { 11 | value := 42 12 | ptr := To(value) 13 | assert.NotNil(t, ptr) 14 | assert.Equal(t, value, *ptr) 15 | }) 16 | 17 | t.Run("should return pointer to string", func(t *testing.T) { 18 | value := "hello" 19 | ptr := To(value) 20 | assert.NotNil(t, ptr) 21 | assert.Equal(t, value, *ptr) 22 | }) 23 | 24 | t.Run("should return pointer to struct", func(t *testing.T) { 25 | type TestStruct struct { 26 | Name string 27 | Age int 28 | } 29 | value := TestStruct{Name: "Alice", Age: 30} 30 | ptr := To(value) 31 | assert.NotNil(t, ptr) 32 | assert.Equal(t, value, *ptr) 33 | }) 34 | 35 | t.Run("should return pointer to zero value", func(t *testing.T) { 36 | var value int 37 | ptr := To(value) 38 | assert.NotNil(t, ptr) 39 | assert.Equal(t, 0, *ptr) 40 | }) 41 | 42 | t.Run("should allow creating pointer to literal", func(t *testing.T) { 43 | ptr := To(100) 44 | assert.NotNil(t, ptr) 45 | assert.Equal(t, 100, *ptr) 46 | }) 47 | } 48 | 49 | func TestDeref(t *testing.T) { 50 | t.Run("should return value when pointer is not nil", func(t *testing.T) { 51 | value := 42 52 | ptr := &value 53 | result := Deref(ptr, 0) 54 | assert.Equal(t, 42, result) 55 | }) 56 | 57 | t.Run("should return default when pointer is nil", func(t *testing.T) { 58 | var ptr *int 59 | result := Deref(ptr, 99) 60 | assert.Equal(t, 99, result) 61 | }) 62 | 63 | t.Run("should work with strings", func(t *testing.T) { 64 | value := "hello" 65 | ptr := &value 66 | result := Deref(ptr, "default") 67 | assert.Equal(t, "hello", result) 68 | 69 | var nilPtr *string 70 | result = Deref(nilPtr, "default") 71 | assert.Equal(t, "default", result) 72 | }) 73 | 74 | t.Run("should work with structs", func(t *testing.T) { 75 | type TestStruct struct { 76 | Name string 77 | } 78 | value := TestStruct{Name: "Alice"} 79 | ptr := &value 80 | defaultValue := TestStruct{Name: "Default"} 81 | 82 | result := Deref(ptr, defaultValue) 83 | assert.Equal(t, "Alice", result.Name) 84 | 85 | var nilPtr *TestStruct 86 | result = Deref(nilPtr, defaultValue) 87 | assert.Equal(t, "Default", result.Name) 88 | }) 89 | 90 | t.Run("should work with zero values", func(t *testing.T) { 91 | zero := 0 92 | ptr := &zero 93 | result := Deref(ptr, 42) 94 | assert.Equal(t, 0, result) 95 | }) 96 | } 97 | 98 | func TestEqual(t *testing.T) { 99 | t.Run("should return true when both are nil", func(t *testing.T) { 100 | var a, b *int 101 | assert.True(t, Equal(a, b)) 102 | }) 103 | 104 | t.Run("should return false when one is nil", func(t *testing.T) { 105 | value := 42 106 | ptr := &value 107 | var nilPtr *int 108 | assert.False(t, Equal(ptr, nilPtr)) 109 | assert.False(t, Equal(nilPtr, ptr)) 110 | }) 111 | 112 | t.Run("should return true when both point to equal values", func(t *testing.T) { 113 | a := 42 114 | b := 42 115 | assert.True(t, Equal(&a, &b)) 116 | }) 117 | 118 | t.Run("should return false when values differ", func(t *testing.T) { 119 | a := 42 120 | b := 43 121 | assert.False(t, Equal(&a, &b)) 122 | }) 123 | 124 | t.Run("should work with strings", func(t *testing.T) { 125 | a := "hello" 126 | b := "hello" 127 | c := "world" 128 | assert.True(t, Equal(&a, &b)) 129 | assert.False(t, Equal(&a, &c)) 130 | }) 131 | } 132 | 133 | func TestCoalesce(t *testing.T) { 134 | t.Run("should return first non-nil pointer", func(t *testing.T) { 135 | var a, b *int 136 | c := 42 137 | d := 99 138 | result := Coalesce(a, b, &c, &d) 139 | assert.NotNil(t, result) 140 | assert.Equal(t, 42, *result) 141 | }) 142 | 143 | t.Run("should return nil when all are nil", func(t *testing.T) { 144 | var a, b, c *int 145 | result := Coalesce(a, b, c) 146 | assert.Nil(t, result) 147 | }) 148 | 149 | t.Run("should return first when first is non-nil", func(t *testing.T) { 150 | a := 1 151 | b := 2 152 | result := Coalesce(&a, &b) 153 | assert.NotNil(t, result) 154 | assert.Equal(t, 1, *result) 155 | }) 156 | 157 | t.Run("should work with no arguments", func(t *testing.T) { 158 | result := Coalesce[int]() 159 | assert.Nil(t, result) 160 | }) 161 | } 162 | 163 | func TestIsNil(t *testing.T) { 164 | t.Run("should return true for nil pointer", func(t *testing.T) { 165 | var ptr *int 166 | assert.True(t, IsNil(ptr)) 167 | }) 168 | 169 | t.Run("should return false for non-nil pointer", func(t *testing.T) { 170 | value := 42 171 | assert.False(t, IsNil(&value)) 172 | }) 173 | } 174 | 175 | func TestToSlice(t *testing.T) { 176 | t.Run("should return empty slice for nil", func(t *testing.T) { 177 | var ptr *int 178 | result := ToSlice(ptr) 179 | assert.Empty(t, result) 180 | assert.NotNil(t, result) 181 | }) 182 | 183 | t.Run("should return slice with single element", func(t *testing.T) { 184 | value := 42 185 | result := ToSlice(&value) 186 | assert.Equal(t, []int{42}, result) 187 | }) 188 | 189 | t.Run("should work with strings", func(t *testing.T) { 190 | value := "hello" 191 | result := ToSlice(&value) 192 | assert.Equal(t, []string{"hello"}, result) 193 | }) 194 | } 195 | 196 | func TestDerefSlice(t *testing.T) { 197 | t.Run("should dereference all non-nil pointers", func(t *testing.T) { 198 | a := 1 199 | b := 2 200 | c := 3 201 | ptrs := []*int{&a, &b, &c} 202 | result := DerefSlice(ptrs, 0) 203 | assert.Equal(t, []int{1, 2, 3}, result) 204 | }) 205 | 206 | t.Run("should use default for nil pointers", func(t *testing.T) { 207 | a := 1 208 | var b *int 209 | c := 3 210 | ptrs := []*int{&a, b, &c} 211 | result := DerefSlice(ptrs, 99) 212 | assert.Equal(t, []int{1, 99, 3}, result) 213 | }) 214 | 215 | t.Run("should handle empty slice", func(t *testing.T) { 216 | var ptrs []*int 217 | result := DerefSlice(ptrs, 0) 218 | assert.Empty(t, result) 219 | assert.NotNil(t, result) 220 | }) 221 | 222 | t.Run("should work with strings", func(t *testing.T) { 223 | a := "hello" 224 | var b *string 225 | c := "world" 226 | ptrs := []*string{&a, b, &c} 227 | result := DerefSlice(ptrs, "default") 228 | assert.Equal(t, []string{"hello", "default", "world"}, result) 229 | }) 230 | } 231 | 232 | func TestNonNilSlice(t *testing.T) { 233 | t.Run("should filter out nil pointers", func(t *testing.T) { 234 | a := 1 235 | var b *int 236 | c := 3 237 | var d *int 238 | e := 5 239 | ptrs := []*int{&a, b, &c, d, &e} 240 | result := NonNilSlice(ptrs) 241 | assert.Len(t, result, 3) 242 | assert.Equal(t, 1, *result[0]) 243 | assert.Equal(t, 3, *result[1]) 244 | assert.Equal(t, 5, *result[2]) 245 | }) 246 | 247 | t.Run("should return empty slice when all are nil", func(t *testing.T) { 248 | var a, b, c *int 249 | ptrs := []*int{a, b, c} 250 | result := NonNilSlice(ptrs) 251 | assert.Empty(t, result) 252 | assert.NotNil(t, result) 253 | }) 254 | 255 | t.Run("should return all when none are nil", func(t *testing.T) { 256 | a := 1 257 | b := 2 258 | c := 3 259 | ptrs := []*int{&a, &b, &c} 260 | result := NonNilSlice(ptrs) 261 | assert.Len(t, result, 3) 262 | }) 263 | 264 | t.Run("should handle empty slice", func(t *testing.T) { 265 | var ptrs []*int 266 | result := NonNilSlice(ptrs) 267 | assert.Empty(t, result) 268 | assert.NotNil(t, result) 269 | }) 270 | } 271 | 272 | func TestToPointerSlice(t *testing.T) { 273 | t.Run("should convert all values to pointers", func(t *testing.T) { 274 | values := []int{1, 2, 3, 4, 5} 275 | result := ToPointerSlice(values) 276 | assert.Len(t, result, 5) 277 | for i, ptr := range result { 278 | assert.NotNil(t, ptr) 279 | assert.Equal(t, values[i], *ptr) 280 | } 281 | }) 282 | 283 | t.Run("should handle empty slice", func(t *testing.T) { 284 | var values []int 285 | result := ToPointerSlice(values) 286 | assert.Empty(t, result) 287 | assert.NotNil(t, result) 288 | }) 289 | 290 | t.Run("should work with strings", func(t *testing.T) { 291 | values := []string{"hello", "world"} 292 | result := ToPointerSlice(values) 293 | assert.Len(t, result, 2) 294 | assert.Equal(t, "hello", *result[0]) 295 | assert.Equal(t, "world", *result[1]) 296 | }) 297 | 298 | t.Run("should work with zero values", func(t *testing.T) { 299 | values := []int{0, 0, 0} 300 | result := ToPointerSlice(values) 301 | assert.Len(t, result, 3) 302 | for _, ptr := range result { 303 | assert.NotNil(t, ptr) 304 | assert.Equal(t, 0, *ptr) 305 | } 306 | }) 307 | } 308 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Go Utils 2 | site_description: Functional programming utilities for Go 1.21+ - complementary with stdlib slices, maps, and cmp 3 | site_url: https://goldziher.github.io/go-utils/ 4 | site_author: Na'aman Hirschfeld 5 | repo_name: Goldziher/go-utils 6 | repo_url: https://github.com/Goldziher/go-utils 7 | edit_uri: edit/main/docs/ 8 | copyright: Copyright © 2023-2025 Na'aman Hirschfeld 9 | 10 | theme: 11 | name: material 12 | logo: assets/logo.png 13 | favicon: assets/favicon.png 14 | palette: 15 | - media: "(prefers-color-scheme: light)" 16 | scheme: default 17 | primary: indigo 18 | accent: lime 19 | toggle: 20 | icon: material/weather-night 21 | name: Switch to dark mode 22 | - media: "(prefers-color-scheme: dark)" 23 | scheme: slate 24 | primary: indigo 25 | accent: lime 26 | toggle: 27 | icon: material/weather-sunny 28 | name: Switch to light mode 29 | font: 30 | text: Roboto 31 | code: Roboto Mono 32 | features: 33 | - content.code.copy 34 | - content.code.annotate 35 | - navigation.indexes 36 | - navigation.sections 37 | - navigation.tabs 38 | - navigation.tabs.sticky 39 | - navigation.tracking 40 | - navigation.top 41 | - navigation.footer 42 | - search.highlight 43 | - search.share 44 | - search.suggest 45 | - toc.follow 46 | - announce.dismiss 47 | icon: 48 | repo: fontawesome/brands/github 49 | language: en 50 | 51 | markdown_extensions: 52 | - admonition 53 | - attr_list 54 | - def_list 55 | - footnotes 56 | - md_in_html 57 | - pymdownx.details 58 | - pymdownx.emoji: 59 | emoji_index: !!python/name:material.extensions.emoji.twemoji 60 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 61 | - pymdownx.highlight: 62 | anchor_linenums: true 63 | line_spans: __span 64 | pygments_lang_class: true 65 | linenums: true 66 | use_pygments: true 67 | - pymdownx.inlinehilite 68 | - pymdownx.keys 69 | - pymdownx.mark 70 | - pymdownx.smartsymbols 71 | - pymdownx.snippets: 72 | base_path: ["."] 73 | check_paths: true 74 | - pymdownx.superfences 75 | - pymdownx.tabbed: 76 | alternate_style: true 77 | combine_header_slug: true 78 | slugify: !!python/object/apply:pymdownx.slugs.slugify 79 | kwds: 80 | case: lower 81 | - pymdownx.tasklist: 82 | custom_checkbox: true 83 | - tables 84 | - toc: 85 | permalink: true 86 | toc_depth: 3 87 | 88 | plugins: 89 | - search 90 | - social 91 | - git-revision-date-localized: 92 | enable_creation_date: true 93 | type: timeago 94 | 95 | extra: 96 | social: 97 | - icon: fontawesome/brands/github 98 | link: https://github.com/Goldziher/go-utils 99 | - icon: fontawesome/brands/golang 100 | link: https://pkg.go.dev/github.com/Goldziher/go-utils 101 | 102 | extra_css: 103 | - css/extra.css 104 | 105 | nav: 106 | - Home: index.md 107 | - Packages: 108 | - sliceutils: 109 | - Overview: sliceutils/index.md 110 | - Functional Operations: 111 | - Filter: sliceutils/filter.md 112 | - Map: sliceutils/map.md 113 | - Reduce: sliceutils/reduce.md 114 | - ForEach: sliceutils/forEach.md 115 | - Search Operations: 116 | - Find: sliceutils/find.md 117 | - FindIndex: sliceutils/findIndex.md 118 | - FindIndexOf: sliceutils/findIndexOf.md 119 | - FindIndexes: sliceutils/findIndexes.md 120 | - FindIndexesOf: sliceutils/findIndexesOf.md 121 | - FindLastIndex: sliceutils/findLastIndex.md 122 | - FindLastIndexOf: sliceutils/findLastIndexOf.md 123 | - Includes: sliceutils/includes.md 124 | - Predicates: 125 | - Some: sliceutils/some.md 126 | - Every: sliceutils/every.md 127 | - Set Operations: 128 | - Merge: sliceutils/merge.md 129 | - Union: sliceutils/union.md 130 | - Intersection: sliceutils/intersection.md 131 | - Difference: sliceutils/difference.md 132 | - Unique: sliceutils/unique.md 133 | - Grouping & Analysis: 134 | - GroupBy: sliceutils/groupBy.md 135 | - Partition: sliceutils/partition.md 136 | - DistinctBy: sliceutils/distinctBy.md 137 | - CountBy: sliceutils/countBy.md 138 | - Selection: 139 | - Insert: sliceutils/insert.md 140 | - Take: sliceutils/take.md 141 | - Skip: sliceutils/skip.md 142 | - TakeLast: sliceutils/takeLast.md 143 | - SkipLast: sliceutils/skipLast.md 144 | - TakeWhile: sliceutils/takeWhile.md 145 | - SkipWhile: sliceutils/skipWhile.md 146 | - Aggregation: 147 | - MinBy: sliceutils/minBy.md 148 | - MaxBy: sliceutils/maxBy.md 149 | - Accessors: 150 | - Head: sliceutils/head.md 151 | - Tail: sliceutils/tail.md 152 | - Last: sliceutils/last.md 153 | - Initial: sliceutils/initial.md 154 | - Transformations: 155 | - Reverse: sliceutils/reverse.md 156 | - Flatten: sliceutils/flatten.md 157 | - FlatMap: sliceutils/flatMap.md 158 | - Chunk: sliceutils/chunk.md 159 | - Pluck: sliceutils/pluck.md 160 | - Utilities: 161 | - Copy: sliceutils/copy.md 162 | - Remove: sliceutils/remove.md 163 | - EnsureUniqueAndAppend: sliceutils/ensureUniqueAndAppend.md 164 | - Sum: sliceutils/sum.md 165 | - Compact: sliceutils/compact.md 166 | - Windows: sliceutils/windows.md 167 | - maputils: 168 | - Overview: maputils/index.md 169 | - Basic Operations: 170 | - Keys: maputils/keys.md 171 | - Values: maputils/values.md 172 | - Filter: maputils/filter.md 173 | - ForEach: maputils/forEach.md 174 | - Merge: maputils/merge.md 175 | - Drop: maputils/drop.md 176 | - Copy: maputils/copy.md 177 | - Transformations: 178 | - Map: maputils/map.md 179 | - MapKeys: maputils/mapKeys.md 180 | - Invert: maputils/invert.md 181 | - Selection: 182 | - Pick: maputils/pick.md 183 | - Omit: maputils/omit.md 184 | - Has: maputils/has.md 185 | - Get: maputils/get.md 186 | - Conversions: 187 | - ToEntries: maputils/toEntries.md 188 | - FromEntries: maputils/fromEntries.md 189 | - Grouping: 190 | - GroupBy: maputils/groupBy.md 191 | - mathutils: 192 | - Overview: mathutils/index.md 193 | - Comparison: 194 | - Clamp: mathutils/clamp.md 195 | - InRange: mathutils/inRange.md 196 | - Arithmetic: 197 | - Abs: mathutils/abs.md 198 | - Average: mathutils/average.md 199 | - Gcd: mathutils/gcd.md 200 | - Lcm: mathutils/lcm.md 201 | - Number Theory: 202 | - IsPrime: mathutils/isPrime.md 203 | - ptrutils: 204 | - Overview: ptrutils/index.md 205 | - Creation: 206 | - To: ptrutils/to.md 207 | - ToSlice: ptrutils/toSlice.md 208 | - ToPointerSlice: ptrutils/toPointerSlice.md 209 | - Dereferencing: 210 | - Deref: ptrutils/deref.md 211 | - DerefSlice: ptrutils/derefSlice.md 212 | - Utilities: 213 | - Equal: ptrutils/equal.md 214 | - Coalesce: ptrutils/coalesce.md 215 | - IsNil: ptrutils/isNil.md 216 | - NonNilSlice: ptrutils/nonNilSlice.md 217 | - excutils: 218 | - Overview: excutils/index.md 219 | - Panic Handling: 220 | - Must: excutils/must.md 221 | - MustResult: excutils/mustResult.md 222 | - Try: excutils/try.md 223 | - Catch: excutils/catch.md 224 | - RecoverWithValue: excutils/recoverWithValue.md 225 | - Error Handling: 226 | - IgnoreErr: excutils/ignoreErr.md 227 | - FirstErr: excutils/firstErr.md 228 | - AllErr: excutils/allErr.md 229 | - ReturnAnyErr: excutils/returnAnyErr.md 230 | - Validation: 231 | - ReturnNotNil: excutils/returnNotNil.md 232 | - MustNotNil: excutils/mustNotNil.md 233 | - Retry Logic: 234 | - Retry: excutils/retry.md 235 | - RetryWithResult: excutils/retryWithResult.md 236 | - stringutils: 237 | - Overview: stringutils/index.md 238 | - Stringify: stringutils/stringify.md 239 | - Capitalize: stringutils/capitalize.md 240 | - PadLeft: stringutils/padLeft.md 241 | - PadRight: stringutils/padRight.md 242 | - structutils: 243 | - Overview: structutils/index.md 244 | - ToMap: structutils/toMap.md 245 | - ForEach: structutils/forEach.md 246 | - dateutils: 247 | - Overview: dateutils/index.md 248 | - Floor: dateutils/floor.md 249 | - Ceil: dateutils/ceil.md 250 | - BeforeOrEqual: dateutils/beforeorequal.md 251 | - AfterOrEqual: dateutils/afterorequal.md 252 | - Overlap: dateutils/overlap.md 253 | - urlutils: 254 | - Overview: urlutils/index.md 255 | - QueryStringifyMap: urlutils/queryStringifyMap.md 256 | - QueryStringifyStruct: urlutils/queryStringifyStruct.md 257 | - Contributing: contributing.md 258 | -------------------------------------------------------------------------------- /stringutils/stringutils_test.go: -------------------------------------------------------------------------------- 1 | package stringutils_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/Goldziher/go-utils/stringutils" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type stringerType struct{} 12 | 13 | func (s stringerType) String() string { 14 | return "Stringer" 15 | } 16 | 17 | type goStringerType struct{} 18 | 19 | func (s goStringerType) GoString() string { 20 | return "GoStringer" 21 | } 22 | 23 | var nilMap map[string]any 24 | var nilSlice []string 25 | 26 | func TestStringify(t *testing.T) { 27 | testCases := []struct { 28 | input any 29 | expectedOutput string 30 | options stringutils.Options 31 | }{ 32 | { 33 | "abc", 34 | "abc", 35 | stringutils.Options{}, 36 | }, 37 | { 38 | true, 39 | "true", 40 | stringutils.Options{}, 41 | }, 42 | { 43 | false, 44 | "false", 45 | stringutils.Options{}, 46 | }, 47 | { 48 | []byte("abc"), 49 | "abc", 50 | stringutils.Options{}, 51 | }, 52 | { 53 | nil, 54 | stringutils.DefaultNilFormat, 55 | stringutils.Options{}, 56 | }, 57 | { 58 | nil, 59 | "null", 60 | stringutils.Options{NilFormat: "null"}, 61 | }, 62 | { 63 | 1, 64 | "1", 65 | stringutils.Options{}, 66 | }, 67 | { 68 | 10, 69 | "1010", 70 | stringutils.Options{Base: 2}, 71 | }, 72 | { 73 | int8(1), 74 | "1", 75 | stringutils.Options{}, 76 | }, 77 | { 78 | int16(1), 79 | "1", 80 | stringutils.Options{}, 81 | }, 82 | { 83 | int32(1), 84 | "1", 85 | stringutils.Options{}, 86 | }, 87 | { 88 | int64(1), 89 | "1", 90 | stringutils.Options{}, 91 | }, 92 | { 93 | uint(1), 94 | "1", 95 | stringutils.Options{}, 96 | }, 97 | { 98 | uint8(1), 99 | "1", 100 | stringutils.Options{}, 101 | }, 102 | { 103 | uint16(1), 104 | "1", 105 | stringutils.Options{}, 106 | }, 107 | { 108 | uint32(1), 109 | "1", 110 | stringutils.Options{}, 111 | }, 112 | { 113 | uint64(1), 114 | "1", 115 | stringutils.Options{}, 116 | }, 117 | { 118 | float32(1), 119 | "1.00", 120 | stringutils.Options{}, 121 | }, 122 | { 123 | float32(1), 124 | "1.0000", 125 | stringutils.Options{Precision: 4}, 126 | }, 127 | { 128 | float32(1), 129 | "1.00E+00", 130 | stringutils.Options{Format: 'E'}, 131 | }, 132 | { 133 | float64(1), 134 | "1.00", 135 | stringutils.Options{}, 136 | }, 137 | { 138 | complex64(1), 139 | "(1.00+0.00i)", 140 | stringutils.Options{}, 141 | }, 142 | { 143 | complex128(1), 144 | "(1.00+0.00i)", 145 | stringutils.Options{}, 146 | }, 147 | { 148 | stringerType{}, 149 | "Stringer", 150 | stringutils.Options{}, 151 | }, 152 | { 153 | goStringerType{}, 154 | "GoStringer", 155 | stringutils.Options{}, 156 | }, 157 | { 158 | nilMap, 159 | stringutils.DefaultNilMapFormat, 160 | stringutils.Options{}, 161 | }, 162 | { 163 | nilMap, 164 | stringutils.DefaultNilFormat, 165 | stringutils.Options{NilMapFormat: stringutils.DefaultNilFormat}, 166 | }, 167 | { 168 | map[string]int{"one": 1, "two": 2}, 169 | "{one: 1, two: 2}", 170 | stringutils.Options{}, 171 | }, 172 | { 173 | nilSlice, 174 | stringutils.DefaultNilSliceFormat, 175 | stringutils.Options{}, 176 | }, 177 | { 178 | nilSlice, 179 | stringutils.DefaultNilFormat, 180 | stringutils.Options{NilSliceFormat: stringutils.DefaultNilFormat}, 181 | }, 182 | { 183 | []int{1, 2, 3}, 184 | "[1, 2, 3]", 185 | stringutils.Options{}, 186 | }, 187 | { 188 | struct { 189 | X int 190 | Y int 191 | }{X: 1, Y: 10}, 192 | "{1 10}", 193 | stringutils.Options{}, 194 | }, 195 | } 196 | 197 | for _, testCase := range testCases { 198 | t.Run( 199 | fmt.Sprintf( 200 | "TestCase: input: %v, expected: %v", 201 | testCase.input, 202 | testCase.expectedOutput, 203 | ), 204 | func(t *testing.T) { 205 | assert.Equal( 206 | t, 207 | testCase.expectedOutput, 208 | stringutils.Stringify(testCase.input, testCase.options), 209 | ) 210 | }, 211 | ) 212 | } 213 | } 214 | 215 | func TestPadLeft(t *testing.T) { 216 | assert.Equal(t, "_-_Azer", stringutils.PadLeft("Azer", "_-", 7)) 217 | assert.Equal(t, "Test", stringutils.PadLeft("Test", "Großmeister", 0)) 218 | assert.Equal(t, "Test", stringutils.PadLeft("Test", "Großmeister", -1)) 219 | assert.Equal(t, "Test", stringutils.PadLeft("Test", "", 7)) 220 | assert.Equal(t, "GroTest", stringutils.PadLeft("Test", "Großmeister", 7)) 221 | assert.Equal(t, "Gro\xc3Test", stringutils.PadLeft("Test", "Großmeister", 8)) 222 | assert.Equal(t, "GroßTest", stringutils.PadLeft("Test", "Großmeister", 9)) 223 | assert.Equal(t, "GroßmeisterGroßTest", stringutils.PadLeft("Test", "Großmeister", 21)) 224 | } 225 | 226 | func TestPadRight(t *testing.T) { 227 | assert.Equal(t, "Azer-_-", stringutils.PadRight("Azer", "-_", 7)) 228 | assert.Equal(t, "Test", stringutils.PadRight("Test", "Großmeister", 0)) 229 | assert.Equal(t, "Test", stringutils.PadRight("Test", "Großmeister", -1)) 230 | assert.Equal(t, "Test", stringutils.PadRight("Test", "", 7)) 231 | assert.Equal(t, "TestGro", stringutils.PadRight("Test", "Großmeister", 7)) 232 | assert.Equal(t, "TestGro\xc3", stringutils.PadRight("Test", "Großmeister", 8)) 233 | assert.Equal(t, "TestGroß", stringutils.PadRight("Test", "Großmeister", 9)) 234 | assert.Equal(t, "TestGroßmeisterGroß", stringutils.PadRight("Test", "Großmeister", 21)) 235 | } 236 | 237 | func TestCapitalize(t *testing.T) { 238 | testCases := []struct { 239 | input string 240 | expected string 241 | }{ 242 | { 243 | input: "store", 244 | expected: "Store", 245 | }, 246 | { 247 | input: "batman", 248 | expected: "Batman", 249 | }, 250 | { 251 | input: "", 252 | expected: "", 253 | }, 254 | { 255 | input: "rusty", 256 | expected: "Rusty", 257 | }, 258 | } 259 | 260 | for _, testCase := range testCases { 261 | t.Run( 262 | fmt.Sprintf("TestCase: input: %v, expected: %v", testCase.input, testCase.expected), 263 | func(t *testing.T) { 264 | assert.Equal(t, testCase.expected, stringutils.Capitalize(testCase.input)) 265 | }, 266 | ) 267 | } 268 | } 269 | 270 | func TestReverse(t *testing.T) { 271 | assert.Equal(t, "olleh", stringutils.Reverse("hello")) 272 | assert.Equal(t, "!dlrow", stringutils.Reverse("world!")) 273 | assert.Equal(t, "", stringutils.Reverse("")) 274 | assert.Equal(t, "a", stringutils.Reverse("a")) 275 | // UTF-8 test 276 | assert.Equal(t, "👋🌍", stringutils.Reverse("🌍👋")) 277 | } 278 | 279 | func TestTruncate(t *testing.T) { 280 | assert.Equal(t, "hello", stringutils.Truncate("hello", 10, "...")) 281 | assert.Equal(t, "hello...", stringutils.Truncate("hello world", 5, "...")) 282 | assert.Equal(t, "...", stringutils.Truncate("test", 0, "...")) 283 | assert.Equal(t, "hello [more]", stringutils.Truncate("hello world", 5, " [more]")) 284 | } 285 | 286 | func TestContains(t *testing.T) { 287 | assert.True(t, stringutils.Contains("hello world", "world", false)) 288 | assert.False(t, stringutils.Contains("hello world", "WORLD", false)) 289 | assert.True(t, stringutils.Contains("hello world", "WORLD", true)) 290 | assert.True(t, stringutils.Contains("Hello World", "hello", true)) 291 | assert.False(t, stringutils.Contains("test", "xyz", false)) 292 | } 293 | 294 | func TestSplitAndTrim(t *testing.T) { 295 | result := stringutils.SplitAndTrim(" hello , world , foo ", ",") 296 | assert.Equal(t, []string{"hello", "world", "foo"}, result) 297 | 298 | result = stringutils.SplitAndTrim("a, , b, ,c", ",") 299 | assert.Equal(t, []string{"a", "b", "c"}, result) 300 | 301 | result = stringutils.SplitAndTrim("", ",") 302 | assert.Empty(t, result) 303 | } 304 | 305 | func TestJoinNonEmpty(t *testing.T) { 306 | assert.Equal(t, "hello,world", stringutils.JoinNonEmpty([]string{"hello", "", "world"}, ",")) 307 | assert.Equal(t, "a-b-c", stringutils.JoinNonEmpty([]string{"a", "b", "c"}, "-")) 308 | assert.Equal(t, "test", stringutils.JoinNonEmpty([]string{"", "test", ""}, ",")) 309 | assert.Equal(t, "", stringutils.JoinNonEmpty([]string{}, ",")) 310 | } 311 | 312 | func TestDefaultIfEmpty(t *testing.T) { 313 | assert.Equal(t, "default", stringutils.DefaultIfEmpty("", "default")) 314 | assert.Equal(t, "default", stringutils.DefaultIfEmpty(" ", "default")) 315 | assert.Equal(t, "hello", stringutils.DefaultIfEmpty("hello", "default")) 316 | assert.Equal(t, "hello", stringutils.DefaultIfEmpty(" hello ", "default")) 317 | } 318 | 319 | func TestToCamelCase(t *testing.T) { 320 | assert.Equal(t, "helloWorld", stringutils.ToCamelCase("hello world")) 321 | assert.Equal(t, "helloWorldFoo", stringutils.ToCamelCase("hello-world-foo")) 322 | assert.Equal(t, "helloWorldBar", stringutils.ToCamelCase("hello_world_bar")) 323 | assert.Equal(t, "test", stringutils.ToCamelCase("test")) 324 | assert.Equal(t, "", stringutils.ToCamelCase("")) 325 | assert.Equal(t, "firstName", stringutils.ToCamelCase("first name")) 326 | assert.Equal(t, "", stringutils.ToCamelCase("___")) 327 | assert.Equal(t, "", stringutils.ToCamelCase("---")) 328 | } 329 | 330 | func TestToSnakeCase(t *testing.T) { 331 | assert.Equal(t, "hello_world", stringutils.ToSnakeCase("HelloWorld")) 332 | assert.Equal(t, "hello_world", stringutils.ToSnakeCase("helloWorld")) 333 | assert.Equal(t, "hello_world", stringutils.ToSnakeCase("hello-world")) 334 | assert.Equal(t, "hello_world", stringutils.ToSnakeCase("hello world")) 335 | assert.Equal(t, "test", stringutils.ToSnakeCase("test")) 336 | assert.Equal(t, "first_name", stringutils.ToSnakeCase("firstName")) 337 | assert.Equal(t, "http_response", stringutils.ToSnakeCase("HTTPResponse")) 338 | } 339 | 340 | func TestToKebabCase(t *testing.T) { 341 | assert.Equal(t, "hello-world", stringutils.ToKebabCase("HelloWorld")) 342 | assert.Equal(t, "hello-world", stringutils.ToKebabCase("helloWorld")) 343 | assert.Equal(t, "hello-world", stringutils.ToKebabCase("hello_world")) 344 | assert.Equal(t, "hello-world", stringutils.ToKebabCase("hello world")) 345 | assert.Equal(t, "test", stringutils.ToKebabCase("test")) 346 | assert.Equal(t, "first-name", stringutils.ToKebabCase("firstName")) 347 | assert.Equal(t, "http-response", stringutils.ToKebabCase("HTTPResponse")) 348 | } 349 | 350 | func TestRemoveWhitespace(t *testing.T) { 351 | assert.Equal(t, "helloworld", stringutils.RemoveWhitespace("hello world")) 352 | assert.Equal(t, "test", stringutils.RemoveWhitespace(" t e s t ")) 353 | assert.Equal(t, "foo", stringutils.RemoveWhitespace("\tfoo\n")) 354 | assert.Equal(t, "", stringutils.RemoveWhitespace(" ")) 355 | } 356 | 357 | func TestEllipsisMiddle(t *testing.T) { 358 | assert.Equal(t, "hello", stringutils.EllipsisMiddle("hello", 10)) 359 | assert.Equal(t, "hel...rld", stringutils.EllipsisMiddle("hello world", 9)) 360 | assert.Equal(t, "he...ld", stringutils.EllipsisMiddle("hello world", 7)) 361 | assert.Equal(t, "...", stringutils.EllipsisMiddle("hello", 3)) 362 | assert.Equal(t, "..", stringutils.EllipsisMiddle("hello", 2)) 363 | } 364 | --------------------------------------------------------------------------------