From 83bb818ade7f559745bbcec730cc7f8c09a901b2 Mon Sep 17 00:00:00 2001 From: tx7do Date: Mon, 6 Nov 2023 13:54:50 +0800 Subject: [PATCH] feat: dateutils, maputils, sliceutils. --- dateutils/dateutils.go | 32 +++ dateutils/dateutils_test.go | 66 ++++++ go.mod | 1 + go.sum | 2 + maputils/maputils.go | 94 +++++++++ maputils/maputils_test.go | 139 +++++++++++++ sliceutils/sliceutils.go | 375 ++++++++++++++++++++++++++++++++++ sliceutils/sliceutils_test.go | 301 +++++++++++++++++++++++++++ tag.bat | 2 +- 9 files changed, 1011 insertions(+), 1 deletion(-) create mode 100644 dateutils/dateutils.go create mode 100644 dateutils/dateutils_test.go create mode 100644 maputils/maputils.go create mode 100644 maputils/maputils_test.go create mode 100644 sliceutils/sliceutils.go create mode 100644 sliceutils/sliceutils_test.go diff --git a/dateutils/dateutils.go b/dateutils/dateutils.go new file mode 100644 index 0000000..4780df1 --- /dev/null +++ b/dateutils/dateutils.go @@ -0,0 +1,32 @@ +package dateutils + +import "time" + +// Floor - takes a datetime and return a datetime from the same day at 00:00:00 (UTC). +func Floor(t time.Time) time.Time { + return t.UTC().Truncate(time.Hour * 24) +} + +// Ceil - takes a datetime and return a datetime from the same day at 23:59:59 (UTC). +func Ceil(t time.Time) time.Time { + // add 24 hours so that we are dealing with tomorrow's datetime + // Floor + // Substract one second and we have today at 23:59:59 + return Floor(t.Add(time.Hour * 24)).Add(time.Second * -1) +} + +func BeforeOrEqual(milestone time.Time, date time.Time) bool { + return date.UTC().Before(milestone) || date.UTC().Equal(milestone) +} + +func AfterOrEqual(milestone time.Time, date time.Time) bool { + return date.UTC().After(milestone) || date.UTC().Equal(milestone) +} + +// Overlap - returns true if two date intervals overlap. +func Overlap(start1 time.Time, end1 time.Time, start2 time.Time, end2 time.Time) bool { + return (AfterOrEqual(start2, start1) && BeforeOrEqual(end2, start1)) || + (AfterOrEqual(start2, end1) && BeforeOrEqual(end2, end1)) || + (AfterOrEqual(start1, start2) && BeforeOrEqual(end1, start2)) || + (AfterOrEqual(start1, end2) && BeforeOrEqual(end1, end2)) +} diff --git a/dateutils/dateutils_test.go b/dateutils/dateutils_test.go new file mode 100644 index 0000000..79f70d5 --- /dev/null +++ b/dateutils/dateutils_test.go @@ -0,0 +1,66 @@ +package dateutils_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/tx7do/go-utils/dateutils" +) + +func TestFloor(t *testing.T) { + now := time.Now() + assert.Equal(t, "00:00:00", dateutils.Floor(now).Format("15:04:05")) +} + +func TestCeil(t *testing.T) { + now := time.Now() + assert.Equal(t, "23:59:59", dateutils.Ceil(now).Format("15:04:05")) +} + +func TestBeforeOrEqual(t *testing.T) { + milestone, _ := time.Parse("2006-01-02", "2023-01-01") + + dBefore, _ := time.Parse("2006-01-02", "2022-12-31") + dEqual, _ := time.Parse("2006-01-02", "2023-01-01") + dAfter, _ := time.Parse("2006-01-02", "2023-01-31") + + assert.Equal(t, true, dateutils.BeforeOrEqual(milestone, dBefore)) + assert.Equal(t, true, dateutils.BeforeOrEqual(milestone, dEqual)) + assert.Equal(t, false, dateutils.BeforeOrEqual(milestone, dAfter)) +} + +func TestAfterOrEqual(t *testing.T) { + milestone, _ := time.Parse("2006-01-02", "2023-01-01") + + dBefore, _ := time.Parse("2006-01-02", "2022-12-31") + dEqual, _ := time.Parse("2006-01-02", "2023-01-01") + dAfter, _ := time.Parse("2006-01-02", "2023-01-31") + + assert.Equal(t, false, dateutils.AfterOrEqual(milestone, dBefore)) + assert.Equal(t, true, dateutils.AfterOrEqual(milestone, dEqual)) + assert.Equal(t, true, dateutils.AfterOrEqual(milestone, dAfter)) +} + +func TestOverlap(t *testing.T) { + s1, _ := time.Parse("2006-01-02", "2022-12-28") + e1, _ := time.Parse("2006-01-02", "2022-12-31") + + s2, _ := time.Parse("2006-01-02", "2022-12-30") + e2, _ := time.Parse("2006-01-02", "2023-01-01") + + s3, _ := time.Parse("2006-01-02", "2023-01-02") + e3, _ := time.Parse("2006-01-02", "2023-01-04") + + assert.Equal(t, true, dateutils.Overlap(s1, e1, s2, e2)) + assert.Equal(t, false, dateutils.Overlap(s1, e1, s3, e3)) + + s4, _ := time.Parse("2006-01-02", "2023-07-13") + e4, _ := time.Parse("2006-01-02", "2023-07-14") + + s5, _ := time.Parse("2006-01-02", "2023-07-10") + e5, _ := time.Parse("2006-01-02", "2023-07-17") + + assert.Equal(t, true, dateutils.Overlap(s4, e4, s5, e5)) + assert.Equal(t, true, dateutils.Overlap(s5, e5, s4, e4)) +} diff --git a/go.mod b/go.mod index 8cf4d38..98d0268 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/sony/sonyflake v1.2.0 github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.14.0 + golang.org/x/exp v0.0.0-20231006140011-7918f672742d ) require ( diff --git a/go.sum b/go.sum index 65ebba9..3d6fbc9 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/maputils/maputils.go b/maputils/maputils.go new file mode 100644 index 0000000..7d3cd2e --- /dev/null +++ b/maputils/maputils.go @@ -0,0 +1,94 @@ +// This package includes utility functions for handling and manipulating maputils. +// It draws inspiration from JavaScript and Python and uses Go generics as a basis. + +package maputils + +// Keys - takes a map with keys K and values V, returns a slice of type K of the map's keys. +// Note: Go maps do not preserve insertion order. +func Keys[K comparable, V any](mapInstance map[K]V) []K { + keys := make([]K, len(mapInstance)) + + i := 0 + for k := range mapInstance { + keys[i] = k + i++ + } + + return keys +} + +// Values - takes a map with keys K and values V, returns a slice of type V of the map's values. +// Note: Go maps do not preserve insertion order. +func Values[K comparable, V any](mapInstance map[K]V) []V { + values := make([]V, len(mapInstance)) + + i := 0 + for _, v := range mapInstance { + values[i] = v + i++ + } + + return values +} + +// Merge - takes an arbitrary number of map instances with keys K and values V and merges them into a single map. +// Note: merge works from left to right. If a key already exists in a previous map, its value is over-written. +func Merge[K comparable, V any](mapInstances ...map[K]V) map[K]V { + var mergedMapSize int + + for _, mapInstance := range mapInstances { + mergedMapSize += len(mapInstance) + } + + mergedMap := make(map[K]V, mergedMapSize) + + for _, mapInstance := range mapInstances { + for k, v := range mapInstance { + mergedMap[k] = v + } + } + + return mergedMap +} + +// ForEach - given a map with keys K and values V, executes the passed in function for each key-value pair. +func ForEach[K comparable, V any](mapInstance map[K]V, function func(key K, value V)) { + for key, value := range mapInstance { + function(key, value) + } +} + +// 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. +// 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. +func Drop[K comparable, V any](mapInstance map[K]V, keys []K) map[K]V { + for _, key := range keys { + delete(mapInstance, key) + } + + return mapInstance +} + +// Copy - takes a map with keys K and values V and returns a copy of the map. +func Copy[K comparable, V any](mapInstance map[K]V) map[K]V { + mapCopy := make(map[K]V, len(mapInstance)) + + for key, value := range mapInstance { + mapCopy[key] = value + } + + return mapCopy +} + +// Filter - takes a map with keys K and values V, and executes the passed in function for each key-value pair. +// If the filter function returns true, the key-value pair will be included in the output, otherwise it is filtered out. +func Filter[K comparable, V any](mapInstance map[K]V, function func(key K, value V) bool) map[K]V { + mapCopy := make(map[K]V, len(mapInstance)) + + for key, value := range mapInstance { + if function(key, value) { + mapCopy[key] = value + } + } + + return mapCopy +} diff --git a/maputils/maputils_test.go b/maputils/maputils_test.go new file mode 100644 index 0000000..64e1d30 --- /dev/null +++ b/maputils/maputils_test.go @@ -0,0 +1,139 @@ +package maputils_test + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/tx7do/go-utils/maputils" +) + +func TestKeys(t *testing.T) { + var daysMap = map[string]int{ + "Sunday": 1, + "Monday": 2, + "Tuesday": 3, + "Wednesday": 4, + "Thursday": 5, + "Friday": 6, + "Saturday": 7, + } + keys := maputils.Keys(daysMap) + days := []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} + sort.Strings(days) + sort.Strings(keys) + assert.Equal(t, days, keys) +} + +func TestValues(t *testing.T) { + var daysMap = map[string]int{ + "Sunday": 1, + "Monday": 2, + "Tuesday": 3, + "Wednesday": 4, + "Thursday": 5, + "Friday": 6, + "Saturday": 7, + } + values := maputils.Values(daysMap) + sort.Ints(values) + assert.Equal(t, []int{1, 2, 3, 4, 5, 6, 7}, values) +} + +func TestMerge(t *testing.T) { + first := map[string]string{ + "George": "Harrison", + "Betty": "Davis", + } + second := map[string]string{ + "Ronald": "Reagen", + "Betty": "Stewart", + } + + assert.Equal(t, + map[string]string{ + "George": "Harrison", + "Betty": "Stewart", + "Ronald": "Reagen", + }, maputils.Merge(first, second)) +} + +func TestForEach(t *testing.T) { + var daysMap = map[string]int{ + "Sunday": 1, + "Monday": 2, + "Tuesday": 3, + "Wednesday": 4, + "Thursday": 5, + "Friday": 6, + "Saturday": 7, + } + + sum := 0 + + maputils.ForEach(daysMap, func(_ string, day int) { + sum += day + }) + + assert.Equal(t, 28, sum) +} + +func TestDrop(t *testing.T) { + var daysMap = map[string]int{ + "Sunday": 1, + "Monday": 2, + "Tuesday": 3, + "Wednesday": 4, + "Thursday": 5, + "Friday": 6, + "Saturday": 7, + } + + expectedResult := map[string]int{ + "Sunday": 1, + "Friday": 6, + } + assert.Equal(t, expectedResult, maputils.Drop(daysMap, []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Saturday"})) + // ensure we do not modify the original value + assert.Equal(t, expectedResult, daysMap) +} + +func TestCopy(t *testing.T) { + var daysMap = map[string]int{ + "Sunday": 1, + "Monday": 2, + "Tuesday": 3, + "Wednesday": 4, + "Thursday": 5, + "Friday": 6, + "Saturday": 7, + } + daysCopy := maputils.Copy(daysMap) + assert.Equal(t, daysMap, daysCopy) + delete(daysCopy, "Sunday") + assert.NotEqual(t, daysMap, daysCopy) +} + +func TestFilter(t *testing.T) { + var daysMap = map[string]int{ + "Sunday": 1, + "Monday": 2, + "Tuesday": 3, + "Wednesday": 4, + "Thursday": 5, + "Friday": 6, + "Saturday": 7, + } + + var expectedResult = map[string]int{ + "Monday": 2, + "Wednesday": 4, + "Friday": 6, + } + + filteredDays := maputils.Filter(daysMap, func(_ string, value int) bool { + return value%2 == 0 + }) + + assert.Equal(t, expectedResult, filteredDays) +} diff --git a/sliceutils/sliceutils.go b/sliceutils/sliceutils.go new file mode 100644 index 0000000..a13b98c --- /dev/null +++ b/sliceutils/sliceutils.go @@ -0,0 +1,375 @@ +// This package includes utility functions for handling and manipulating a slice. +// It draws inspiration from JavaScript and Python and uses Go generics as a basis. + +package sliceutils + +import ( + "golang.org/x/exp/constraints" +) + +// Filter - given a slice of type T, executes the given predicate function on each element in the slice. +// The predicate is passed the current element, the current index and the slice itself as function arguments. +// If the predicate returns true, the value is included in the result, otherwise it is filtered out. +func Filter[T any](slice []T, predicate func(value T, index int, slice []T) bool) (filtered []T) { + for i, el := range slice { + if ok := predicate(el, i, slice); ok { + filtered = append(filtered, el) + } + } + return filtered +} + +// ForEach - given a slice of type T, executes the passed in function for each element in the slice. +// The function is passed the current element, the current index and the slice itself as function arguments. +func ForEach[T any](slice []T, function func(value T, index int, slice []T)) { + for i, el := range slice { + function(el, i, slice) + } +} + +// Map - given a slice of type T, executes the passed in mapper function for each element in the slice, returning a slice of type R. +// The function is passed the current element, the current index and the slice itself as function arguments. +func Map[T any, R any](slice []T, mapper func(value T, index int, slice []T) R) (mapped []R) { + if len(slice) > 0 { + mapped = make([]R, len(slice)) + for i, el := range slice { + mapped[i] = mapper(el, i, slice) + } + } + return mapped +} + +// Reduce - given a slice of type T, executes the passed in reducer function for each element in the slice, returning a result of type R. +// The function is passed the accumulator, current element, current index and the slice itself as function arguments. +// The third argument to reduce is the initial value of type R to use. +func Reduce[T any, R any](slice []T, reducer func(acc R, value T, index int, slice []T) R, initial R) R { + acc := initial + for i, el := range slice { + acc = reducer(acc, el, i, slice) + } + return acc +} + +// Find - given a slice of type T, executes the passed in predicate function for each element in the slice. +// If the predicate returns true - a pointer to the element is returned. If no element is found, nil is returned. +// The function is passed the current element, the current index and the slice itself as function arguments. +func Find[T any](slice []T, predicate func(value T, index int, slice []T) bool) *T { + for i, el := range slice { + if ok := predicate(el, i, slice); ok { + return &el + } + } + return nil +} + +// FindIndex - given a slice of type T, executes the passed in predicate function for each element in the slice. +// If the predicate returns true - the index of the element is returned. If no element is found, -1 is returned. +// The function is passed the current element, the current index and the slice itself as function arguments. +func FindIndex[T any](slice []T, predicate func(value T, index int, slice []T) bool) int { + for i, el := range slice { + if ok := predicate(el, i, slice); ok { + return i + } + } + return -1 +} + +// FindIndexOf - given a slice of type T and a value of type T, return ths first index of an element equal to value. +// If no element is found, -1 is returned. +func FindIndexOf[T comparable](slice []T, value T) int { + for i, el := range slice { + if el == value { + return i + } + } + return -1 +} + +// FindLastIndex - given a slice of type T, executes the passed in predicate function for each element in the slice starting from its end. +// If no element is found, -1 is returned. The function is passed the current element, the current index and the slice itself as function arguments. +func FindLastIndex[T any](slice []T, predicate func(value T, index int, slice []T) bool) int { + for i := len(slice) - 1; i > 0; i-- { + el := slice[i] + if ok := predicate(el, i, slice); ok { + return i + } + } + return -1 +} + +// FindLastIndexOf - given a slice of type T and a value of type T, returning the last index matching the given value. +// If no element is found, -1 is returned. +func FindLastIndexOf[T comparable](slice []T, value T) int { + for i := len(slice) - 1; i > 0; i-- { + el := slice[i] + if el == value { + return i + } + } + return -1 +} + +// FindIndexes - given a slice of type T, executes the passed in predicate function for each element in the slice. +// Returns a slice containing all indexes of elements for which the predicate returned true. If no element are found, returns a nil int slice. +// The function is passed the current element, the current index and the slice itself as function arguments. +func FindIndexes[T any](slice []T, predicate func(value T, index int, slice []T) bool) []int { + var indexes []int + for i, el := range slice { + if ok := predicate(el, i, slice); ok { + indexes = append(indexes, i) + } + } + return indexes +} + +// FindIndexesOf - given a slice of type T and a value of type T, returns a slice containing all indexes match the given value. +// If no element are found, returns a nil int slice. +func FindIndexesOf[T comparable](slice []T, value T) []int { + var indexes []int + for i, el := range slice { + if el == value { + indexes = append(indexes, i) + } + } + return indexes +} + +// Includes - given a slice of type T and a value of type T, determines whether the value is contained by the slice. +// Note: T is constrained to comparable types only and comparison is determined using the equality operator. +func Includes[T comparable](slice []T, value T) bool { + for _, el := range slice { + if el == value { + return true + } + } + return false +} + +// Some - given a slice of type T, executes the given predicate for each element of the slice. +// If the predicate returns true for any element, it returns true, otherwise it returns false. +// The function is passed the current element, the current index and the slice itself as function arguments. +func Some[T any](slice []T, predicate func(value T, index int, slice []T) bool) bool { + for i, el := range slice { + if ok := predicate(el, i, slice); ok { + return true + } + } + return false +} + +// Every - given a slice of type T, executes the given predicate for each element of the slice. +// If the predicate returns true for all elements, it returns true, otherwise it returns false. +// The function is passed the current element, the current index and the slice itself as function arguments. +func Every[T any](slice []T, predicate func(value T, index int, slice []T) bool) bool { + for i, el := range slice { + if ok := predicate(el, i, slice); !ok { + return false + } + } + return true +} + +// Merge - receives slices of type T and merges them into a single slice of type T. +// Note: The elements are merged in their order in a slice, +// i.e. first the elements of the first slice, then that of the second and so forth. +func Merge[T any](slices ...[]T) (mergedSlice []T) { + if len(slices) > 0 { + mergedSliceCap := 0 + + for _, slice := range slices { + mergedSliceCap += len(slice) + } + + if mergedSliceCap > 0 { + mergedSlice = make([]T, 0, mergedSliceCap) + + for _, slice := range slices { + mergedSlice = append(mergedSlice, slice...) + } + } + } + return mergedSlice +} + +// Sum - receives a slice of type T and returns a value T that is the sum of the numbers. +// Note: T is constrained to be a number type. +func Sum[T constraints.Complex | constraints.Integer | constraints.Float](slice []T) (result T) { + for _, el := range slice { + result += el + } + return result +} + +// Remove - receives a slice of type T and an index, removing the element at the given index. +// Note: this function does not modify the input slice. +func Remove[T any](slice []T, i int) []T { + if len(slice) == 0 || i > len(slice)-1 { + return slice + } + copied := Copy(slice) + if i == 0 { + return copied[1:] + } + if i != len(copied)-1 { + return append(copied[:i], copied[i+1:]...) + } + return copied[:i] +} + +// Insert - receives a slice of type T, an index and a value. +// The value is inserted at the given index. If there is an existing value at this index, it is shifted to the next index. +// Note: this function does not modify the input slice. +func Insert[T any](slice []T, i int, value T) []T { + if len(slice) == i { + return append(slice, value) + } + slice = append(slice[:i+1], slice[i:]...) + slice[i] = value + return slice +} + +// Copy - receives a slice of type T and copies it. +func Copy[T any](slice []T) []T { + duplicate := make([]T, len(slice), cap(slice)) + copy(duplicate, slice) + return duplicate +} + +// Intersection - takes a variadic number of slices of type T and returns a slice of type T containing any values that exist in all the slices. +// For example, given []int{1, 2, 3}, []int{1, 7, 3}, the intersection would be []int{1, 3}. +func Intersection[T comparable](slices ...[]T) []T { + possibleIntersections := map[T]int{} + for i, slice := range slices { + for _, el := range slice { + if i == 0 { + possibleIntersections[el] = 0 + } else if _, elementExists := possibleIntersections[el]; elementExists { + possibleIntersections[el] = i + } + } + } + + intersected := make([]T, 0) + for _, el := range slices[0] { + if lastVisitorIndex, exists := possibleIntersections[el]; exists && lastVisitorIndex == len(slices)-1 { + intersected = append(intersected, el) + delete(possibleIntersections, el) + } + } + + return intersected +} + +// Difference - takes a variadic number of slices of type T and returns a slice of type T containing the elements that are different between the slices. +// For example, given []int{1, 2, 3}, []int{2, 3, 4}, []{3, 4, 5}, the difference would be []int{1, 5}. +func Difference[T comparable](slices ...[]T) []T { + possibleDifferences := map[T]int{} + nonDifferentElements := map[T]int{} + + for i, slice := range slices { + for _, el := range slice { + if lastVisitorIndex, elementExists := possibleDifferences[el]; elementExists && lastVisitorIndex != i { + nonDifferentElements[el] = i + } else if !elementExists { + possibleDifferences[el] = i + } + } + } + + differentElements := make([]T, 0) + + for _, slice := range slices { + for _, el := range slice { + if _, exists := nonDifferentElements[el]; !exists { + differentElements = append(differentElements, el) + } + } + } + + return differentElements +} + +// Union - takes a variadic number of slices of type T and returns a slice of type T containing the unique elements in the different slices +// For example, given []int{1, 2, 3}, []int{2, 3, 4}, []int{3, 4, 5}, the union would be []int{1, 2, 3, 4, 5}. +func Union[T comparable](slices ...[]T) []T { + return Unique(Merge(slices...)) +} + +// Reverse - takes a slice of type T and returns a slice of type T with a reverse order of elements. +func Reverse[T any](slice []T) []T { + result := make([]T, len(slice)) + + itemCount := len(slice) + middle := itemCount / 2 + result[middle] = slice[middle] + + for i := 0; i < middle; i++ { + mirrorIdx := itemCount - i - 1 + result[i], result[mirrorIdx] = slice[mirrorIdx], slice[i] + } + return result +} + +// Unique - receives a slice of type T and returns a slice of type T containing all unique elements. +func Unique[T comparable](slice []T) []T { + unique := make([]T, 0) + visited := map[T]bool{} + + for _, value := range slice { + if exists := visited[value]; !exists { + unique = append(unique, value) + visited[value] = true + } + } + return unique +} + +// Chunk - receives a slice of type T and size N and returns a slice of slices T of size N. +func Chunk[T any](input []T, size int) [][]T { + var chunks [][]T + + for i := 0; i < len(input); i += size { + end := i + size + if end > len(input) { + end = len(input) + } + chunks = append(chunks, input[i:end]) + } + return chunks +} + +// Pluck - receives a slice of type I and a getter func to a field +// and returns a slice containing the requested field's value from each item in the slice. +func Pluck[I any, O any](input []I, getter func(I) *O) []O { + var output []O + + for _, item := range input { + field := getter(item) + + if field != nil { + output = append(output, *field) + } + } + + return output +} + +// Flatten - receives a slice of slices of type I and flattens it to a slice of type I. +func Flatten[I any](input [][]I) (output []I) { + if len(input) > 0 { + var outputSize int + + for _, item := range input { + outputSize += len(item) + } + + if outputSize > 0 { + output = make([]I, 0, outputSize) + + for _, item := range input { + output = append(output, item...) + } + } + } + return output +} diff --git a/sliceutils/sliceutils_test.go b/sliceutils/sliceutils_test.go new file mode 100644 index 0000000..167a1cf --- /dev/null +++ b/sliceutils/sliceutils_test.go @@ -0,0 +1,301 @@ +package sliceutils_test + +import ( + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/tx7do/go-utils/sliceutils" +) + +type MyInt int + +type Pluckable struct { + Code string + Value string +} + +var numerals = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} +var numeralsWithUserDefinedType = []MyInt{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} +var days = []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} +var lastNames = []string{"Jacobs", "Vin", "Jacobs", "Smith"} + +func TestFilter(t *testing.T) { + expectedResult := []int{0, 2, 4, 6, 8} + actualResult := sliceutils.Filter(numerals, func(value int, _ int, _ []int) bool { + return value%2 == 0 + }) + assert.Equal(t, expectedResult, actualResult) +} + +func TestForEach(t *testing.T) { + result := 0 + sliceutils.ForEach(numerals, func(value int, _ int, _ []int) { + result += value + }) + assert.Equal(t, 45, result) +} + +func TestMap(t *testing.T) { + expectedResult := []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"} + actualResult := sliceutils.Map(numerals, func(value int, _ int, _ []int) string { + return strconv.Itoa(value) + }) + assert.Equal(t, expectedResult, actualResult) + + assert.Nil(t, sliceutils.Map([]int{}, func(_ int, _ int, _ []int) string { + return "" + })) + + assert.Nil(t, sliceutils.Map([]int(nil), func(_ int, _ int, _ []int) string { + return "" + })) +} + +func TestReduce(t *testing.T) { + expectedResult := map[string]string{"result": "0123456789"} + actualResult := sliceutils.Reduce( + numerals, + func(acc map[string]string, cur int, _ int, _ []int) map[string]string { + acc["result"] += strconv.Itoa(cur) + return acc + }, + map[string]string{"result": ""}, + ) + assert.Equal(t, expectedResult, actualResult) +} + +func TestFind(t *testing.T) { + expectedResult := "Wednesday" + actualResult := sliceutils.Find(days, func(value string, index int, slice []string) bool { + return strings.Contains(value, "Wed") + }) + assert.Equal(t, expectedResult, *actualResult) + assert.Nil(t, sliceutils.Find(days, func(value string, index int, slice []string) bool { + return strings.Contains(value, "Rishon") + })) +} + +func TestFindIndex(t *testing.T) { + expectedResult := 3 + actualResult := sliceutils.FindIndex(days, func(value string, index int, slice []string) bool { + return strings.Contains(value, "Wed") + }) + assert.Equal(t, expectedResult, actualResult) + assert.Equal(t, -1, sliceutils.FindIndex(days, func(value string, index int, slice []string) bool { + return strings.Contains(value, "Rishon") + })) +} + +func TestFindIndexOf(t *testing.T) { + expectedResult := 3 + actualResult := sliceutils.FindIndexOf(days, "Wednesday") + assert.Equal(t, expectedResult, actualResult) + assert.Equal(t, -1, sliceutils.FindIndexOf(days, "Rishon")) +} + +func TestFindLastIndex(t *testing.T) { + expectedResult := 2 + actualResult := sliceutils.FindLastIndex(lastNames, func(value string, index int, slice []string) bool { + return value == "Jacobs" + }) + assert.Equal(t, expectedResult, actualResult) + assert.Equal(t, -1, sliceutils.FindLastIndex(lastNames, func(value string, index int, slice []string) bool { + return value == "Hamudi" + })) +} + +func TestFindLastIndexOf(t *testing.T) { + expectedResult := 2 + actualResult := sliceutils.FindLastIndexOf(lastNames, "Jacobs") + assert.Equal(t, expectedResult, actualResult) + assert.Equal(t, -1, sliceutils.FindLastIndexOf(lastNames, "Hamudi")) +} + +func TestFindIndexes(t *testing.T) { + expectedResult := []int{0, 2} + actualResult := sliceutils.FindIndexes(lastNames, func(value string, index int, slice []string) bool { + return value == "Jacobs" + }) + assert.Equal(t, expectedResult, actualResult) + assert.Nil(t, sliceutils.FindIndexes(lastNames, func(value string, index int, slice []string) bool { + return value == "Hamudi" + })) +} + +func TestFindIndexesOf(t *testing.T) { + expectedResult := []int{0, 2} + actualResult := sliceutils.FindIndexesOf(lastNames, "Jacobs") + assert.Equal(t, expectedResult, actualResult) + assert.Nil(t, sliceutils.FindIndexesOf(lastNames, "Hamudi")) +} + +func TestIncludes(t *testing.T) { + assert.True(t, sliceutils.Includes(numerals, 1)) + assert.False(t, sliceutils.Includes(numerals, 11)) +} + +func TestAny(t *testing.T) { + assert.True(t, sliceutils.Some(numerals, func(value int, _ int, _ []int) bool { + return value%5 == 0 + })) + assert.False(t, sliceutils.Some(numerals, func(value int, _ int, _ []int) bool { + return value == 11 + })) +} + +func TestAll(t *testing.T) { + assert.True(t, sliceutils.Every([]int{1, 1, 1}, func(value int, _ int, _ []int) bool { + return value == 1 + })) + assert.False(t, sliceutils.Every([]int{1, 1, 1, 2}, func(value int, _ int, _ []int) bool { + return value == 1 + })) +} + +func TestMerge(t *testing.T) { + result := sliceutils.Merge(numerals[:5], numerals[5:]) + assert.Equal(t, numerals, result) + + assert.Nil(t, sliceutils.Merge([]int(nil), []int(nil))) + assert.Nil(t, sliceutils.Merge([]int{}, []int{})) +} + +func TestSum(t *testing.T) { + result := sliceutils.Sum(numerals) + assert.Equal(t, 45, result) +} + +func TestSum2(t *testing.T) { + result := sliceutils.Sum(numeralsWithUserDefinedType) + assert.Equal(t, MyInt(45), result) +} + +func TestRemove(t *testing.T) { + testSlice := []int{1, 2, 3} + result := sliceutils.Remove(testSlice, 1) + assert.Equal(t, []int{1, 3}, result) + assert.Equal(t, []int{1, 2, 3}, testSlice) + result = sliceutils.Remove(result, 1) + assert.Equal(t, []int{1}, result) + result = sliceutils.Remove(result, 3) + assert.Equal(t, []int{1}, result) + result = sliceutils.Remove(result, 0) + assert.Equal(t, []int{}, result) + result = sliceutils.Remove(result, 1) + assert.Equal(t, []int{}, result) +} + +func TestCopy(t *testing.T) { + testSlice := []int{1, 2, 3} + copiedSlice := sliceutils.Copy(testSlice) + copiedSlice[0] = 2 + assert.NotEqual(t, testSlice, copiedSlice) +} + +func TestInsert(t *testing.T) { + testSlice := []int{1, 2} + result := sliceutils.Insert(testSlice, 0, 3) + assert.Equal(t, []int{3, 1, 2}, result) + assert.NotEqual(t, testSlice, result) + assert.Equal(t, []int{1, 3, 2}, sliceutils.Insert(testSlice, 1, 3)) + assert.Equal(t, []int{1, 2, 3}, sliceutils.Insert(testSlice, 2, 3)) +} + +func TestIntersection(t *testing.T) { + expectedResult := []int{3, 4, 5} + + first := []int{1, 2, 3, 4, 5} + second := []int{2, 3, 4, 5, 6} + third := []int{3, 4, 5, 6, 7} + + assert.Equal(t, expectedResult, sliceutils.Intersection(first, second, third)) +} + +func TestDifference(t *testing.T) { + expectedResult := []int{1, 7} + + first := []int{1, 2, 3, 4, 5} + second := []int{2, 3, 4, 5, 6} + third := []int{3, 4, 5, 6, 7} + + assert.Equal(t, expectedResult, sliceutils.Difference(first, second, third)) +} + +func TestUnion(t *testing.T) { + expectedResult := []int{1, 2, 3, 4, 5, 6, 7} + + first := []int{1, 2, 3, 4, 5} + second := []int{2, 3, 4, 5, 6} + third := []int{3, 4, 5, 6, 7} + + assert.Equal(t, expectedResult, sliceutils.Union(first, second, third)) +} + +func TestReverse(t *testing.T) { + expectedResult := []int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0} + assert.Equal(t, expectedResult, sliceutils.Reverse(numerals)) + // ensure does not modify the original + assert.Equal(t, expectedResult, sliceutils.Reverse(numerals)) + + // test basic odd length case + expectedResult = []int{9, 8, 7, 6, 5, 4, 3, 2, 1} + assert.Equal(t, expectedResult, sliceutils.Reverse(numerals[1:])) +} + +func TestUnique(t *testing.T) { + duplicates := []int{6, 6, 6, 9, 0, 0, 0} + expectedResult := []int{6, 9, 0} + assert.Equal(t, expectedResult, sliceutils.Unique(duplicates)) + // Ensure original is unaltered + assert.NotEqual(t, expectedResult, duplicates) +} + +func TestChunk(t *testing.T) { + numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + + assert.Equal(t, [][]int{{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}}, sliceutils.Chunk(numbers, 2)) + assert.Equal(t, [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10}}, sliceutils.Chunk(numbers, 3)) +} + +func TestPluck(t *testing.T) { + items := []Pluckable{ + { + Code: "azer", + Value: "Azer", + }, + { + Code: "tyuio", + Value: "Tyuio", + }, + } + + assert.Equal(t, []string{"azer", "tyuio"}, sliceutils.Pluck(items, func(item Pluckable) *string { + return &item.Code + })) + assert.Equal(t, []string{"Azer", "Tyuio"}, sliceutils.Pluck(items, func(item Pluckable) *string { + return &item.Value + })) +} + +func TestFlatten(t *testing.T) { + items := [][]int{ + {1, 2, 3, 4}, + {5, 6}, + {7, 8}, + {9, 10, 11}, + } + + flattened := sliceutils.Flatten(items) + + assert.Equal(t, []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, flattened) + + assert.Nil(t, sliceutils.Flatten([][]int{})) + assert.Nil(t, sliceutils.Flatten([][]int(nil))) + + assert.Nil(t, sliceutils.Flatten([][]int{{}, {}})) + assert.Nil(t, sliceutils.Flatten([][]int{nil, nil})) + + assert.Nil(t, sliceutils.Flatten([][]int{{}, nil})) +} diff --git a/tag.bat b/tag.bat index bff08e9..348d678 100644 --- a/tag.bat +++ b/tag.bat @@ -1,4 +1,4 @@ -git tag v1.1.4 +git tag v1.1.5 git tag bank_card/v1.1.0 git tag entgo/v1.1.6 git tag geoip/v1.1.0