From da82d442cc59d2fcffb476fa075a0e7ec4a9d085 Mon Sep 17 00:00:00 2001 From: tx7do Date: Mon, 6 Nov 2023 14:11:20 +0800 Subject: [PATCH] feat: add. --- {dateutils => dateutil}/dateutils.go | 2 +- {dateutils => dateutil}/dateutils_test.go | 28 +-- go.mod | 1 + go.sum | 2 + ioutil/file.go | 12 + ioutil/path.go | 230 +++++++++++++++++-- ioutil/path_test.go | 101 ++++++++ {sliceutils => sliceutil}/sliceutils.go | 2 +- {sliceutils => sliceutil}/sliceutils_test.go | 118 +++++----- structutil/structutils.go | 37 +++ structutil/structutils_test.go | 55 +++++ tag.bat | 2 +- {time => timeutil}/consts.go | 4 + {time => timeutil}/diff.go | 0 {time => timeutil}/diff_test.go | 0 timeutil/format.go | 45 ++++ {time => timeutil}/range.go | 0 {time => timeutil}/range_test.go | 0 {time => timeutil}/trans.go | 0 {time => timeutil}/trans_test.go | 0 20 files changed, 545 insertions(+), 94 deletions(-) rename {dateutils => dateutil}/dateutils.go (98%) rename {dateutils => dateutil}/dateutils_test.go (59%) create mode 100644 ioutil/file.go create mode 100644 ioutil/path_test.go rename {sliceutils => sliceutil}/sliceutils.go (99%) rename {sliceutils => sliceutil}/sliceutils_test.go (56%) create mode 100644 structutil/structutils.go create mode 100644 structutil/structutils_test.go rename {time => timeutil}/consts.go (56%) rename {time => timeutil}/diff.go (100%) rename {time => timeutil}/diff_test.go (100%) create mode 100644 timeutil/format.go rename {time => timeutil}/range.go (100%) rename {time => timeutil}/range_test.go (100%) rename {time => timeutil}/trans.go (100%) rename {time => timeutil}/trans_test.go (100%) diff --git a/dateutils/dateutils.go b/dateutil/dateutils.go similarity index 98% rename from dateutils/dateutils.go rename to dateutil/dateutils.go index 4780df1..3f2ea05 100644 --- a/dateutils/dateutils.go +++ b/dateutil/dateutils.go @@ -1,4 +1,4 @@ -package dateutils +package dateutil import "time" diff --git a/dateutils/dateutils_test.go b/dateutil/dateutils_test.go similarity index 59% rename from dateutils/dateutils_test.go rename to dateutil/dateutils_test.go index 79f70d5..b514b74 100644 --- a/dateutils/dateutils_test.go +++ b/dateutil/dateutils_test.go @@ -1,21 +1,21 @@ -package dateutils_test +package dateutil_test import ( "testing" "time" "github.com/stretchr/testify/assert" - "github.com/tx7do/go-utils/dateutils" + "github.com/tx7do/go-utils/dateutil" ) func TestFloor(t *testing.T) { now := time.Now() - assert.Equal(t, "00:00:00", dateutils.Floor(now).Format("15:04:05")) + assert.Equal(t, "00:00:00", dateutil.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")) + assert.Equal(t, "23:59:59", dateutil.Ceil(now).Format("15:04:05")) } func TestBeforeOrEqual(t *testing.T) { @@ -25,9 +25,9 @@ func TestBeforeOrEqual(t *testing.T) { 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)) + assert.Equal(t, true, dateutil.BeforeOrEqual(milestone, dBefore)) + assert.Equal(t, true, dateutil.BeforeOrEqual(milestone, dEqual)) + assert.Equal(t, false, dateutil.BeforeOrEqual(milestone, dAfter)) } func TestAfterOrEqual(t *testing.T) { @@ -37,9 +37,9 @@ func TestAfterOrEqual(t *testing.T) { 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)) + assert.Equal(t, false, dateutil.AfterOrEqual(milestone, dBefore)) + assert.Equal(t, true, dateutil.AfterOrEqual(milestone, dEqual)) + assert.Equal(t, true, dateutil.AfterOrEqual(milestone, dAfter)) } func TestOverlap(t *testing.T) { @@ -52,8 +52,8 @@ func TestOverlap(t *testing.T) { 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)) + assert.Equal(t, true, dateutil.Overlap(s1, e1, s2, e2)) + assert.Equal(t, false, dateutil.Overlap(s1, e1, s3, e3)) s4, _ := time.Parse("2006-01-02", "2023-07-13") e4, _ := time.Parse("2006-01-02", "2023-07-14") @@ -61,6 +61,6 @@ func TestOverlap(t *testing.T) { 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)) + assert.Equal(t, true, dateutil.Overlap(s4, e4, s5, e5)) + assert.Equal(t, true, dateutil.Overlap(s5, e5, s4, e4)) } diff --git a/go.mod b/go.mod index 98d0268..9af86bf 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/tx7do/go-utils go 1.20 require ( + github.com/gobwas/glob v0.2.3 github.com/google/uuid v1.4.0 github.com/gosimple/slug v1.13.1 github.com/sony/sonyflake v1.2.0 diff --git a/go.sum b/go.sum index 3d6fbc9..2575369 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q= diff --git a/ioutil/file.go b/ioutil/file.go new file mode 100644 index 0000000..956df89 --- /dev/null +++ b/ioutil/file.go @@ -0,0 +1,12 @@ +package ioutil + +import "os" + +// ReadFile 读取文件 +func ReadFile(path string) []byte { + content, err := os.ReadFile(path) + if err != nil { + return nil + } + return content +} diff --git a/ioutil/path.go b/ioutil/path.go index 26e8087..32d7b98 100644 --- a/ioutil/path.go +++ b/ioutil/path.go @@ -1,8 +1,14 @@ package ioutil import ( + "fmt" + "io/ioutil" "os" + "os/user" "path/filepath" + "strings" + + "github.com/gobwas/glob" ) // GetWorkingDirPath 获取工作路径 @@ -33,18 +39,6 @@ func GetAbsPath() string { return dir } -// PathExist 路径是否存在 -func PathExist(path string) bool { - _, err := os.Stat(path) - if err == nil { - return true - } - if os.IsNotExist(err) { - return false - } - return false -} - // GetFileList 获取文件夹下面的所有文件的列表 func GetFileList(root string) []string { var files []string @@ -75,11 +69,211 @@ func GetFolderNameList(root string) []string { return names } -// ReadFile 读取文件 -func ReadFile(path string) []byte { - content, err := os.ReadFile(path) - if err != nil { - return nil +// MatchPath Returns whether a given path matches a glob pattern. +// +// via github.com/gobwas/glob: +// +// Compile creates Glob for given pattern and strings (if any present after pattern) as separators. +// The pattern syntax is: +// +// pattern: +// { term } +// +// term: +// `*` matches any sequence of non-separator characters +// `**` matches any sequence of characters +// `?` matches any single non-separator character +// `[` [ `!` ] { character-range } `]` +// character class (must be non-empty) +// `{` pattern-list `}` +// pattern alternatives +// c matches character c (c != `*`, `**`, `?`, `\`, `[`, `{`, `}`) +// `\` c matches character c +// +// character-range: +// c matches character c (c != `\\`, `-`, `]`) +// `\` c matches character c +// lo `-` hi matches character c for lo <= c <= hi +// +// pattern-list: +// pattern { `,` pattern } +// comma-separated (without spaces) patterns +func MatchPath(pattern string, path string) bool { + if g, err := glob.Compile(pattern); err == nil { + return g.Match(path) + } + + return false +} + +// ExpandUser replaces the tilde (~) in a path into the current user's home directory. +func ExpandUser(path string) (string, error) { + if u, err := user.Current(); err == nil { + fullTilde := fmt.Sprintf("~%s", u.Name) + + if strings.HasPrefix(path, `~/`) || path == `~` { + return strings.Replace(path, `~`, u.HomeDir, 1), nil + } + + if strings.HasPrefix(path, fullTilde+`/`) || path == fullTilde { + return strings.Replace(path, fullTilde, u.HomeDir, 1), nil + } + + return path, nil + } else { + return path, err + } +} + +// IsNonemptyExecutableFile Returns true if the given path is a regular file, is executable by any user, and has a non-zero size. +func IsNonemptyExecutableFile(path string) bool { + if stat, err := os.Stat(path); err == nil && stat.Size() > 0 && (stat.Mode().Perm()&0111) != 0 { + return true + } + + return false +} + +// IsNonemptyFile Returns true if the given path is a regular file with a non-zero size. +func IsNonemptyFile(path string) bool { + if FileExists(path) { + if stat, err := os.Stat(path); err == nil && stat.Size() > 0 { + return true + } + } + + return false +} + +// IsNonemptyDir Returns true if the given path is a directory with items in it. +func IsNonemptyDir(path string) bool { + if DirExists(path) { + if entries, err := ioutil.ReadDir(path); err == nil && len(entries) > 0 { + return true + } + } + + return false +} + +// Exists Returns true if the given path exists. +func Exists(path string) bool { + if _, err := os.Stat(path); err == nil { + return true + } + + return false +} + +// LinkExists Returns true if the given path exists and is a symbolic link. +func LinkExists(path string) bool { + if stat, err := os.Stat(path); err == nil { + return IsSymlink(stat.Mode()) + } + + return false +} + +// FileExists Returns true if the given path exists and is a regular file. +func FileExists(path string) bool { + if stat, err := os.Stat(path); err == nil { + return stat.Mode().IsRegular() + } + + return false +} + +// DirExists Returns true if the given path exists and is a directory. +func DirExists(path string) bool { + if stat, err := os.Stat(path); err == nil { + return stat.IsDir() + } + + return false +} + +// PathExist 路径是否存在 +func PathExist(path string) bool { + _, err := os.Stat(path) + if err == nil { + return true + } + if os.IsNotExist(err) { + return false + } + return false +} + +func IsSymlink(mode os.FileMode) bool { + return mode&os.ModeSymlink != 0 +} + +func IsDevice(mode os.FileMode) bool { + return mode&os.ModeDevice != 0 +} + +func IsCharDevice(mode os.FileMode) bool { + return mode&os.ModeCharDevice != 0 +} + +func IsNamedPipe(mode os.FileMode) bool { + return mode&os.ModeNamedPipe != 0 +} + +func IsSocket(mode os.FileMode) bool { + return mode&os.ModeSocket != 0 +} + +func IsSticky(mode os.FileMode) bool { + return mode&os.ModeSticky != 0 +} + +func IsSetuid(mode os.FileMode) bool { + return mode&os.ModeSetuid != 0 +} + +func IsSetgid(mode os.FileMode) bool { + return mode&os.ModeSetgid != 0 +} + +func IsTemporary(mode os.FileMode) bool { + return mode&os.ModeTemporary != 0 +} + +func IsExclusive(mode os.FileMode) bool { + return mode&os.ModeExclusive != 0 +} + +func IsAppend(mode os.FileMode) bool { + return mode&os.ModeAppend != 0 +} + +// IsReadable Returns true if the given file can be opened for reading by the current user. +func IsReadable(filename string) bool { + if f, err := os.OpenFile(filename, os.O_RDONLY, 0); err == nil { + defer f.Close() + return true + } else { + return false + } +} + +// IsWritable Returns true if the given file can be opened for writing by the current user. +func IsWritable(filename string) bool { + if f, err := os.OpenFile(filename, os.O_WRONLY, 0); err == nil { + defer f.Close() + return true + } else { + return false + } +} + +// IsAppendable Returns true if the given file can be opened for appending by the current user. +func IsAppendable(filename string) bool { + if f, err := os.OpenFile(filename, os.O_APPEND, 0); err == nil { + defer f.Close() + return true + } else { + return false } - return content } diff --git a/ioutil/path_test.go b/ioutil/path_test.go new file mode 100644 index 0000000..50fa672 --- /dev/null +++ b/ioutil/path_test.go @@ -0,0 +1,101 @@ +package ioutil + +import ( + "os" + "os/user" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestExpandUser(t *testing.T) { + assert := require.New(t) + var v string + var err error + + u, _ := user.Current() + + v, err = ExpandUser(`/dev/null`) + assert.Equal(v, `/dev/null`) + assert.Nil(err) + + v, err = ExpandUser(`~`) + assert.Equal(v, u.HomeDir) + assert.Nil(err) + + v, err = ExpandUser("~" + u.Name) + assert.Equal(v, u.HomeDir) + assert.Nil(err) + + v, err = ExpandUser("~/test-123") + assert.Equal(v, u.HomeDir+"/test-123") + assert.Nil(err) + + v, err = ExpandUser("~" + u.Name + "/test-123") + assert.Equal(v, u.HomeDir+"/test-123") + assert.Nil(err) + + v, err = ExpandUser("~/test-123/~/123") + assert.Equal(v, u.HomeDir+"/test-123/~/123") + assert.Nil(err) + + v, err = ExpandUser("~" + u.Name + "/test-123/~" + u.Name + "/123") + assert.Equal(v, u.HomeDir+"/test-123/~"+u.Name+"/123") + assert.Nil(err) + + assert.False(IsNonemptyFile(`/nonexistent.txt`)) + assert.False(IsNonemptyDir(`/nonexistent/dir`)) + + assert.True(IsNonemptyFile(`/etc/hosts`)) + assert.True(IsNonemptyDir(`/etc`)) + + x, err := os.Executable() + assert.NoError(err) + assert.True(IsNonemptyExecutableFile(x)) +} + +func TestMatchPath(t *testing.T) { + require.True(t, MatchPath(`**`, `/hello/there.txt`)) + require.True(t, MatchPath(`*.txt`, `/hello/there.txt`)) + require.True(t, MatchPath(`**/*.txt`, `/hello/there.txt`)) + require.False(t, MatchPath(`**/*.txt`, `/hello/there.jpg`)) + require.True(t, MatchPath("* ?at * eyes", "my cat has very bright eyes")) + require.True(t, MatchPath("", "")) + require.False(t, MatchPath("", "b")) + require.True(t, MatchPath("*ä", "åä")) + require.True(t, MatchPath("abc", "abc")) + require.True(t, MatchPath("a*c", "abc")) + require.True(t, MatchPath("a*c", "a12345c")) + require.True(t, MatchPath("a?c", "a1c")) + require.True(t, MatchPath("?at", "cat")) + require.True(t, MatchPath("?at", "fat")) + require.True(t, MatchPath("*", "abc")) + require.True(t, MatchPath(`\*`, "*")) + require.False(t, MatchPath("?at", "at")) + require.True(t, MatchPath("*test", "this is a test")) + require.True(t, MatchPath("this*", "this is a test")) + require.True(t, MatchPath("*is *", "this is a test")) + require.True(t, MatchPath("*is*a*", "this is a test")) + require.True(t, MatchPath("**test**", "this is a test")) + require.True(t, MatchPath("**is**a***test*", "this is a test")) + require.False(t, MatchPath("*is", "this is a test")) + require.False(t, MatchPath("*no*", "this is a test")) + require.True(t, MatchPath("[!a]*", "this is a test3")) + require.True(t, MatchPath("*abc", "abcabc")) + require.True(t, MatchPath("**abc", "abcabc")) + require.True(t, MatchPath("???", "abc")) + require.True(t, MatchPath("?*?", "abc")) + require.True(t, MatchPath("?*?", "ac")) + require.False(t, MatchPath("sta", "stagnation")) + require.True(t, MatchPath("sta*", "stagnation")) + require.False(t, MatchPath("sta?", "stagnation")) + require.False(t, MatchPath("sta?n", "stagnation")) + require.True(t, MatchPath("{abc,def}ghi", "defghi")) + require.True(t, MatchPath("{abc,abcd}a", "abcda")) + require.True(t, MatchPath("{a,ab}{bc,f}", "abc")) + require.True(t, MatchPath("{*,**}{a,b}", "ab")) + require.False(t, MatchPath("{*,**}{a,b}", "ac")) + require.True(t, MatchPath("/{rate,[a-z][a-z][a-z]}*", "/rate")) + require.True(t, MatchPath("/{rate,[0-9][0-9][0-9]}*", "/rate")) + require.True(t, MatchPath("/{rate,[a-z][a-z][a-z]}*", "/usd")) +} diff --git a/sliceutils/sliceutils.go b/sliceutil/sliceutils.go similarity index 99% rename from sliceutils/sliceutils.go rename to sliceutil/sliceutils.go index a13b98c..01cc4fe 100644 --- a/sliceutils/sliceutils.go +++ b/sliceutil/sliceutils.go @@ -1,7 +1,7 @@ // 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 +package sliceutil import ( "golang.org/x/exp/constraints" diff --git a/sliceutils/sliceutils_test.go b/sliceutil/sliceutils_test.go similarity index 56% rename from sliceutils/sliceutils_test.go rename to sliceutil/sliceutils_test.go index 167a1cf..be949f4 100644 --- a/sliceutils/sliceutils_test.go +++ b/sliceutil/sliceutils_test.go @@ -1,4 +1,4 @@ -package sliceutils_test +package sliceutil_test import ( "strconv" @@ -6,7 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/tx7do/go-utils/sliceutils" + "github.com/tx7do/go-utils/sliceutil" ) type MyInt int @@ -23,7 +23,7 @@ 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 { + actualResult := sliceutil.Filter(numerals, func(value int, _ int, _ []int) bool { return value%2 == 0 }) assert.Equal(t, expectedResult, actualResult) @@ -31,7 +31,7 @@ func TestFilter(t *testing.T) { func TestForEach(t *testing.T) { result := 0 - sliceutils.ForEach(numerals, func(value int, _ int, _ []int) { + sliceutil.ForEach(numerals, func(value int, _ int, _ []int) { result += value }) assert.Equal(t, 45, result) @@ -39,23 +39,23 @@ func TestForEach(t *testing.T) { 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 { + actualResult := sliceutil.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 { + assert.Nil(t, sliceutil.Map([]int{}, func(_ int, _ int, _ []int) string { return "" })) - assert.Nil(t, sliceutils.Map([]int(nil), func(_ int, _ int, _ []int) string { + assert.Nil(t, sliceutil.Map([]int(nil), func(_ int, _ int, _ []int) string { return "" })) } func TestReduce(t *testing.T) { expectedResult := map[string]string{"result": "0123456789"} - actualResult := sliceutils.Reduce( + actualResult := sliceutil.Reduce( numerals, func(acc map[string]string, cur int, _ int, _ []int) map[string]string { acc["result"] += strconv.Itoa(cur) @@ -68,139 +68,139 @@ func TestReduce(t *testing.T) { func TestFind(t *testing.T) { expectedResult := "Wednesday" - actualResult := sliceutils.Find(days, func(value string, index int, slice []string) bool { + actualResult := sliceutil.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 { + assert.Nil(t, sliceutil.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 { + actualResult := sliceutil.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 { + assert.Equal(t, -1, sliceutil.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") + actualResult := sliceutil.FindIndexOf(days, "Wednesday") assert.Equal(t, expectedResult, actualResult) - assert.Equal(t, -1, sliceutils.FindIndexOf(days, "Rishon")) + assert.Equal(t, -1, sliceutil.FindIndexOf(days, "Rishon")) } func TestFindLastIndex(t *testing.T) { expectedResult := 2 - actualResult := sliceutils.FindLastIndex(lastNames, func(value string, index int, slice []string) bool { + actualResult := sliceutil.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 { + assert.Equal(t, -1, sliceutil.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") + actualResult := sliceutil.FindLastIndexOf(lastNames, "Jacobs") assert.Equal(t, expectedResult, actualResult) - assert.Equal(t, -1, sliceutils.FindLastIndexOf(lastNames, "Hamudi")) + assert.Equal(t, -1, sliceutil.FindLastIndexOf(lastNames, "Hamudi")) } func TestFindIndexes(t *testing.T) { expectedResult := []int{0, 2} - actualResult := sliceutils.FindIndexes(lastNames, func(value string, index int, slice []string) bool { + actualResult := sliceutil.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 { + assert.Nil(t, sliceutil.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") + actualResult := sliceutil.FindIndexesOf(lastNames, "Jacobs") assert.Equal(t, expectedResult, actualResult) - assert.Nil(t, sliceutils.FindIndexesOf(lastNames, "Hamudi")) + assert.Nil(t, sliceutil.FindIndexesOf(lastNames, "Hamudi")) } func TestIncludes(t *testing.T) { - assert.True(t, sliceutils.Includes(numerals, 1)) - assert.False(t, sliceutils.Includes(numerals, 11)) + assert.True(t, sliceutil.Includes(numerals, 1)) + assert.False(t, sliceutil.Includes(numerals, 11)) } func TestAny(t *testing.T) { - assert.True(t, sliceutils.Some(numerals, func(value int, _ int, _ []int) bool { + assert.True(t, sliceutil.Some(numerals, func(value int, _ int, _ []int) bool { return value%5 == 0 })) - assert.False(t, sliceutils.Some(numerals, func(value int, _ int, _ []int) bool { + assert.False(t, sliceutil.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 { + assert.True(t, sliceutil.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 { + assert.False(t, sliceutil.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:]) + result := sliceutil.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{})) + assert.Nil(t, sliceutil.Merge([]int(nil), []int(nil))) + assert.Nil(t, sliceutil.Merge([]int{}, []int{})) } func TestSum(t *testing.T) { - result := sliceutils.Sum(numerals) + result := sliceutil.Sum(numerals) assert.Equal(t, 45, result) } func TestSum2(t *testing.T) { - result := sliceutils.Sum(numeralsWithUserDefinedType) + result := sliceutil.Sum(numeralsWithUserDefinedType) assert.Equal(t, MyInt(45), result) } func TestRemove(t *testing.T) { testSlice := []int{1, 2, 3} - result := sliceutils.Remove(testSlice, 1) + result := sliceutil.Remove(testSlice, 1) assert.Equal(t, []int{1, 3}, result) assert.Equal(t, []int{1, 2, 3}, testSlice) - result = sliceutils.Remove(result, 1) + result = sliceutil.Remove(result, 1) assert.Equal(t, []int{1}, result) - result = sliceutils.Remove(result, 3) + result = sliceutil.Remove(result, 3) assert.Equal(t, []int{1}, result) - result = sliceutils.Remove(result, 0) + result = sliceutil.Remove(result, 0) assert.Equal(t, []int{}, result) - result = sliceutils.Remove(result, 1) + result = sliceutil.Remove(result, 1) assert.Equal(t, []int{}, result) } func TestCopy(t *testing.T) { testSlice := []int{1, 2, 3} - copiedSlice := sliceutils.Copy(testSlice) + copiedSlice := sliceutil.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) + result := sliceutil.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)) + assert.Equal(t, []int{1, 3, 2}, sliceutil.Insert(testSlice, 1, 3)) + assert.Equal(t, []int{1, 2, 3}, sliceutil.Insert(testSlice, 2, 3)) } func TestIntersection(t *testing.T) { @@ -210,7 +210,7 @@ func TestIntersection(t *testing.T) { second := []int{2, 3, 4, 5, 6} third := []int{3, 4, 5, 6, 7} - assert.Equal(t, expectedResult, sliceutils.Intersection(first, second, third)) + assert.Equal(t, expectedResult, sliceutil.Intersection(first, second, third)) } func TestDifference(t *testing.T) { @@ -220,7 +220,7 @@ func TestDifference(t *testing.T) { second := []int{2, 3, 4, 5, 6} third := []int{3, 4, 5, 6, 7} - assert.Equal(t, expectedResult, sliceutils.Difference(first, second, third)) + assert.Equal(t, expectedResult, sliceutil.Difference(first, second, third)) } func TestUnion(t *testing.T) { @@ -230,24 +230,24 @@ func TestUnion(t *testing.T) { second := []int{2, 3, 4, 5, 6} third := []int{3, 4, 5, 6, 7} - assert.Equal(t, expectedResult, sliceutils.Union(first, second, third)) + assert.Equal(t, expectedResult, sliceutil.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)) + assert.Equal(t, expectedResult, sliceutil.Reverse(numerals)) // ensure does not modify the original - assert.Equal(t, expectedResult, sliceutils.Reverse(numerals)) + assert.Equal(t, expectedResult, sliceutil.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:])) + assert.Equal(t, expectedResult, sliceutil.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)) + assert.Equal(t, expectedResult, sliceutil.Unique(duplicates)) // Ensure original is unaltered assert.NotEqual(t, expectedResult, duplicates) } @@ -255,8 +255,8 @@ func TestUnique(t *testing.T) { 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)) + assert.Equal(t, [][]int{{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}}, sliceutil.Chunk(numbers, 2)) + assert.Equal(t, [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10}}, sliceutil.Chunk(numbers, 3)) } func TestPluck(t *testing.T) { @@ -271,10 +271,10 @@ func TestPluck(t *testing.T) { }, } - assert.Equal(t, []string{"azer", "tyuio"}, sliceutils.Pluck(items, func(item Pluckable) *string { + assert.Equal(t, []string{"azer", "tyuio"}, sliceutil.Pluck(items, func(item Pluckable) *string { return &item.Code })) - assert.Equal(t, []string{"Azer", "Tyuio"}, sliceutils.Pluck(items, func(item Pluckable) *string { + assert.Equal(t, []string{"Azer", "Tyuio"}, sliceutil.Pluck(items, func(item Pluckable) *string { return &item.Value })) } @@ -287,15 +287,15 @@ func TestFlatten(t *testing.T) { {9, 10, 11}, } - flattened := sliceutils.Flatten(items) + flattened := sliceutil.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, sliceutil.Flatten([][]int{})) + assert.Nil(t, sliceutil.Flatten([][]int(nil))) - assert.Nil(t, sliceutils.Flatten([][]int{{}, {}})) - assert.Nil(t, sliceutils.Flatten([][]int{nil, nil})) + assert.Nil(t, sliceutil.Flatten([][]int{{}, {}})) + assert.Nil(t, sliceutil.Flatten([][]int{nil, nil})) - assert.Nil(t, sliceutils.Flatten([][]int{{}, nil})) + assert.Nil(t, sliceutil.Flatten([][]int{{}, nil})) } diff --git a/structutil/structutils.go b/structutil/structutils.go new file mode 100644 index 0000000..b42fd67 --- /dev/null +++ b/structutil/structutils.go @@ -0,0 +1,37 @@ +package structutil + +import "reflect" + +// ForEach given a struct, calls the passed in function with each visible struct field's name, value and tag. +func ForEach[T any](structInstance T, function func(key string, value any, tag reflect.StructTag)) { + typeOf := reflect.TypeOf(structInstance) + valueOf := reflect.ValueOf(structInstance) + fields := reflect.VisibleFields(typeOf) + + for _, field := range fields { + value := valueOf.FieldByName(field.Name) + function(field.Name, value.Interface(), field.Tag) + } +} + +// ToMap given a struct, converts it to a map[string]any. +// This function also takes struct tag names as optional parameters - if passed in, the struct tags will be used to remap or omit values. +func ToMap[T any](structInstance T, structTags ...string) map[string]any { + output := make(map[string]any, 0) + + ForEach(structInstance, func(key string, value any, tag reflect.StructTag) { + if tag != "" { + for _, s := range structTags { + if tagValue, isPresent := tag.Lookup(s); isPresent && tagValue != "" { + key = tagValue + break + } + } + } + if key != "-" { + output[key] = value + } + }) + + return output +} diff --git a/structutil/structutils_test.go b/structutil/structutils_test.go new file mode 100644 index 0000000..19c042e --- /dev/null +++ b/structutil/structutils_test.go @@ -0,0 +1,55 @@ +package structutil_test + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/tx7do/go-utils/structutil" +) + +type TestStruct struct { + First string + Second int + Third bool `struct:"third"` +} + +func TestForEach(t *testing.T) { + instance := TestStruct{ + "moishe", + 22, + true, + } + structutil.ForEach(instance, func(key string, value any, tag reflect.StructTag) { + switch key { + case "First": + assert.Equal(t, "moishe", value.(string)) + assert.Zero(t, tag) + case "Second": + assert.Equal(t, 22, value.(int)) + assert.Zero(t, tag) + case "Third": + assert.Equal(t, true, value.(bool)) + assert.Equal(t, "third", tag.Get("struct")) + } + }) +} + +func TestToMap(t *testing.T) { + instance := TestStruct{ + "moishe", + 22, + true, + } + assert.Equal(t, map[string]any{ + "First": "moishe", + "Second": 22, + "Third": true, + }, structutil.ToMap(instance)) + + assert.Equal(t, map[string]any{ + "First": "moishe", + "Second": 22, + "third": true, + }, structutil.ToMap(instance, "struct")) +} diff --git a/tag.bat b/tag.bat index 348d678..db7d41c 100644 --- a/tag.bat +++ b/tag.bat @@ -1,4 +1,4 @@ -git tag v1.1.5 +git tag v1.1.6 git tag bank_card/v1.1.0 git tag entgo/v1.1.6 git tag geoip/v1.1.0 diff --git a/time/consts.go b/timeutil/consts.go similarity index 56% rename from time/consts.go rename to timeutil/consts.go index 68dc3ed..04fc24c 100644 --- a/time/consts.go +++ b/timeutil/consts.go @@ -1,5 +1,7 @@ package util +import "time" + const ( DateLayout = "2006-01-02" ClockLayout = "15:04:05" @@ -7,3 +9,5 @@ const ( DefaultTimeLocationName = "Asia/Shanghai" ) + +var ReferenceTimeValue time.Time = time.Date(2006, 1, 2, 15, 4, 5, 999999999, time.FixedZone("MST", -7*60*60)) diff --git a/time/diff.go b/timeutil/diff.go similarity index 100% rename from time/diff.go rename to timeutil/diff.go diff --git a/time/diff_test.go b/timeutil/diff_test.go similarity index 100% rename from time/diff_test.go rename to timeutil/diff_test.go diff --git a/timeutil/format.go b/timeutil/format.go new file mode 100644 index 0000000..0f8e94b --- /dev/null +++ b/timeutil/format.go @@ -0,0 +1,45 @@ +package util + +import ( + "fmt" + "strings" + "time" +) + +// ReferenceTime Return the standard Golang reference time (2006-01-02T15:04:05.999999999Z07:00) +func ReferenceTime() time.Time { + return ReferenceTimeValue +} + +// FormatTimer Formats the given duration in a colon-separated timer format in the form +// [HH:]MM:SS. +func FormatTimer(d time.Duration) string { + h, m, s := DurationHMS(d) + + out := fmt.Sprintf("%02d:%02d:%02d", h, m, s) + out = strings.TrimPrefix(out, `00:`) + out = strings.TrimPrefix(out, `0`) + return out +} + +// FormatTimerf Formats the given duration using the given format string. The string follows +// the same formatting rules as described in the fmt package, and will receive +// three integer arguments: hours, minutes, and seconds. +func FormatTimerf(format string, d time.Duration) string { + h, m, s := DurationHMS(d) + + out := fmt.Sprintf(format, h, m, s) + return out +} + +// DurationHMS Extracts the hours, minutes, and seconds from the given duration. +func DurationHMS(d time.Duration) (int, int, int) { + d = d.Round(time.Second) + h := d / time.Hour + d -= h * time.Hour + m := d / time.Minute + d -= m * time.Minute + s := d / time.Second + + return int(h), int(m), int(s) +} diff --git a/time/range.go b/timeutil/range.go similarity index 100% rename from time/range.go rename to timeutil/range.go diff --git a/time/range_test.go b/timeutil/range_test.go similarity index 100% rename from time/range_test.go rename to timeutil/range_test.go diff --git a/time/trans.go b/timeutil/trans.go similarity index 100% rename from time/trans.go rename to timeutil/trans.go diff --git a/time/trans_test.go b/timeutil/trans_test.go similarity index 100% rename from time/trans_test.go rename to timeutil/trans_test.go