diff --git a/README.md b/README.md index a24aade..105fc30 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

![Go version](https://img.shields.io/badge/go-%3E%3D1.16-9cf) -[![Release](https://img.shields.io/badge/release-1.2.1-green.svg)](https://github.com/duke-git/lancet/releases) +[![Release](https://img.shields.io/badge/release-1.2.2-green.svg)](https://github.com/duke-git/lancet/releases) [![GoDoc](https://godoc.org/github.com//duke-git/lancet?status.svg)](https://pkg.go.dev/github.com/duke-git/lancet) [![Go Report Card](https://goreportcard.com/badge/github.com/duke-git/lancet)](https://goreportcard.com/report/github.com/duke-git/lancet) [![test](https://github.com/duke-git/lancet/actions/workflows/codecov.yml/badge.svg?branch=main&event=push)](https://github.com/duke-git/lancet/actions/workflows/codecov.yml) @@ -278,6 +278,7 @@ func After(n int, fn interface{}) func(args ...interface{}) []reflect.Value //c func Before(n int, fn interface{}) func(args ...interface{}) []reflect.Value //creates a function that invokes func once it's called less than n times func (f Fn) Curry(i interface{}) func(...interface{}) interface{} //make a curryed function func Compose(fnList ...func(...interface{}) interface{}) func(...interface{}) interface{} //compose the functions from right to left +func Debounced(fn func(), duration time.Duration) func() //creates a debounced function that delays invoking fn until after wait duration have elapsed since the last time the debounced function was invoked. func Delay(delay time.Duration, fn interface{}, args ...interface{}) //invoke function after delayed time func Schedule(d time.Duration, fn interface{}, args ...interface{}) chan bool //invoke function every duration time, util close the returned bool chan func (w *Watcher) Start() //start the watch timer. @@ -434,7 +435,10 @@ func main() { func Contain[T comparable](slice []T, value T) bool //check if the value is in the slice or not func ContainSubSlice[T comparable](slice, subslice []T) bool //check if the slice contain subslice or not func Chunk[T any](slice []T, size int) [][]T //creates an slice of elements split into groups the length of size. +func Compact[T any](slice []T) []T //creates an slice with all falsey values removed. The values false, nil, 0, and "" are falsey +func Concat[T any](slice []T, values ...[]T) []T //creates a new slice concatenating slice with any additional slices and/or values func Difference[T comparable](slice1, slice2 []T) []T //creates an slice of whose element not included in the other given slice +func DifferenceBy[T any](slice []T, comparedSlice []T, iteratee func(index int, t T) T) []T //it accepts iteratee which is invoked for each element of slice and values to generate the criterion by which they're compared. func DeleteByIndex[T any](slice []T, start int, end ...int) []T //delete the element of slice from start index to end index - 1 func Drop[T any](slice []T, n int) []T //creates a slice with `n` elements dropped from the beginning when n > 0, or `n` elements dropped from the ending when n < 0 func Every[T any](slice []T, fn func(index int, t T) bool) bool //return true if all of the values in the slice pass the predicate function @@ -496,6 +500,7 @@ func Capitalize(s string) string //convert the first character of a string to up func IsString(v interface{}) bool //check if the value data type is string or not func KebabCase(s string) string //covert string to kebab-case, "foo_Bar" -> "foo-bar" func LowerFirst(s string) string //convert the first character of string to lower case +func UpperFirst(s string) string //converts the first character of string to upper case func PadEnd(source string, size int, padStr string) string //pads string on the right side if it's shorter than size func PadStart(source string, size int, padStr string) string//pads string on the left side if it's shorter than size func ReverseStr(s string) string //return string whose char order is reversed to the given string @@ -586,6 +591,7 @@ func IsIp(ipstr string) bool //check if the string is a ip address func IsIpV4(ipstr string) bool //check if the string is a ipv4 address func IsIpV6(ipstr string) bool //check if the string is a ipv6 address func IsStrongPassword(password string, length int) bool //check if the string is strong password (alpha(lower+upper) + number + special chars(!@#$%^&*()?><)) +func IsUrl(str string) bool //check if the string is url func IsWeakPassword(password string) bool //check if the string is weak password(only letter or only number or letter + number) ``` diff --git a/README_zh-CN.md b/README_zh-CN.md index 81ce7ec..cf74e44 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -4,7 +4,7 @@

![Go version](https://img.shields.io/badge/go-%3E%3D1.16-9cf) -[![Release](https://img.shields.io/badge/release-1.2.1-green.svg)](https://github.com/duke-git/lancet/releases) +[![Release](https://img.shields.io/badge/release-1.2.2-green.svg)](https://github.com/duke-git/lancet/releases) [![GoDoc](https://godoc.org/github.com//duke-git/lancet?status.svg)](https://pkg.go.dev/github.com/duke-git/lancet) [![Go Report Card](https://goreportcard.com/badge/github.com/duke-git/lancet)](https://goreportcard.com/report/github.com/duke-git/lancet) [![test](https://github.com/duke-git/lancet/actions/workflows/codecov.yml/badge.svg?branch=main&event=push)](https://github.com/duke-git/lancet/actions/workflows/codecov.yml) @@ -279,6 +279,7 @@ func Before(n int, fn interface{}) func(args ...interface{}) []reflect.Value // func (f Fn) Curry(i interface{}) func(...interface{}) interface{} //函数柯里化 func Compose(fnList ...func(...interface{}) interface{}) func(...interface{}) interface{} //从右至左组合函数 func Delay(delay time.Duration, fn interface{}, args ...interface{}) //延迟调用函数 +func Debounced(fn func(), duration time.Duration) func() //go防抖函数,在duration时间内连续调用只会执行一次. func Schedule(d time.Duration, fn interface{}, args ...interface{}) chan bool //每隔duration时间调用函数, 关闭返回通道可以停止调用 func (w *Watcher) Start() //开时watcher func (w *Watcher) Stop() //开时watcher @@ -434,7 +435,10 @@ func main() { func Contain[T comparable](slice []T, value T) bool //判断slice是否包含value func ContainSubSlice[T comparable](slice, subslice []T) bool //判断slice是否包含subslice func Chunk[T any](slice []T, size int) [][]T //均分slice +func Compact[T any](slice []T) []T //去除slice中的false vule. false values are false, nil, 0, and "" +func Concat[T any](slice []T, values ...[]T) []T //连接values到slice中 func Difference[T comparable](slice1, slice2 []T) []T //返回切片,其元素在slice1中,不在slice2中 +func DifferenceBy[T any](slice []T, comparedSlice []T, iteratee func(index int, t T) T) []T //将slice 和comparedSlice中每个元素调用iterateeFn后作比较,如果不相等返回slice中的元素。 func DeleteByIndex[T any](slice []T, start int, end ...int) []T //删除切片中start到end位置的值(不包含end) func Drop[T any](slice []T, n int) []T //创建一个新切片,当n大于0时删除原切片前n个元素,当n小于0时删除原切片后n个元素 func Every[T any](slice []T, fn func(index int, t T) bool) bool //slice中所有元素都符合函数条件时返回true, 否则返回false. 函数签名:func(int, t T) bool @@ -495,6 +499,7 @@ func CamelCase(s string) string //字符串转为cameCase, "foo bar" -> "fooBar" func Capitalize(s string) string //字符串转为Capitalize, "fOO" -> "Foo" func IsString(v interface{}) bool //判断是否是字符串 func KebabCase(s string) string //字符串转为KebabCase, "foo_Bar" -> "foo-bar" +func UpperFirst(s string) string //字符串的第一个字母转为大写字母 func LowerFirst(s string) string //字符串的第一个字母转为小写字母 func PadEnd(source string, size int, padStr string) string //字符串末尾填充size个字符 func PadStart(source string, size int, padStr string) string//字符串开头填充size个字符 @@ -587,6 +592,7 @@ func IsIp(ipstr string) bool //判断字符串是否是ip func IsIpV4(ipstr string) bool //判断字符串是否是ipv4 func IsIpV6(ipstr string) bool //判断字符串是否是ipv6 func IsStrongPassword(password string, length int) bool //判断字符串是否是强密码(大小写字母+数字+特殊字符) +func IsUrl(str string) bool //判断字符串是否是url func IsWeakPassword(password string) bool //判断字符串是否是弱密码(只有字母或数字) ``` diff --git a/fileutil/file.go b/fileutil/file.go index f042914..11812a4 100644 --- a/fileutil/file.go +++ b/fileutil/file.go @@ -262,7 +262,7 @@ func FileMode(path string) (fs.FileMode, error) { } // MiMeType return file mime type -// file should be string or *os.File +// param `file` should be string(file path) or *os.File func MiMeType(file interface{}) string { var mediatype string diff --git a/function/function.go b/function/function.go index 437c34e..9171305 100644 --- a/function/function.go +++ b/function/function.go @@ -70,6 +70,23 @@ func Delay(delay time.Duration, fn interface{}, args ...interface{}) { invokeFunc(fn, args...) } +// Debounced creates a debounced function that delays invoking fn until after wait duration have elapsed since the last time the debounced function was invoked. +func Debounced(fn func(), duration time.Duration) func() { + timer := time.NewTimer(duration) + timer.Stop() + + go func() { + for { + select { + case <-timer.C: + go fn() + } + } + }() + + return func() { timer.Reset(duration) } +} + // Schedule invoke function every duration time, util close the returned bool chan func Schedule(d time.Duration, fn interface{}, args ...interface{}) chan bool { // Catch programming error while constructing the closure diff --git a/function/function_test.go b/function/function_test.go index 5ab909b..98f53f5 100644 --- a/function/function_test.go +++ b/function/function_test.go @@ -93,6 +93,28 @@ func TestDelay(t *testing.T) { Delay(2*time.Second, print, "test delay") } +func TestDebounced(t *testing.T) { + assert := internal.NewAssert(t, "TestDebounced") + + count := 0 + add := func() { + count++ + } + + debouncedAdd := Debounced(add, 50*time.Microsecond) + debouncedAdd() + debouncedAdd() + debouncedAdd() + debouncedAdd() + + time.Sleep(100 * time.Millisecond) + assert.Equal(1, count) + + debouncedAdd() + time.Sleep(100 * time.Millisecond) + assert.Equal(2, count) +} + func TestSchedule(t *testing.T) { assert := internal.NewAssert(t, "TestSchedule") diff --git a/slice/slice.go b/slice/slice.go index 4cb12af..bad251e 100644 --- a/slice/slice.go +++ b/slice/slice.go @@ -13,9 +13,9 @@ import ( ) // Contain check if the value is in the iterable type or not -func Contain[T comparable](slice []T, value T) bool { +func Contain[T any](slice []T, value T) bool { for _, v := range slice { - if v == value { + if reflect.DeepEqual(v, value) { return true } } @@ -71,11 +71,14 @@ func Chunk[T any](slice []T, size int) [][]T { return res } -// Difference creates an slice of whose element in slice1 but not in slice2 -func Difference[T comparable](slice1, slice2 []T) []T { - var res []T - for _, v := range slice1 { - if !Contain(slice2, v) { +// Compact creates an slice with all falsey values removed. The values false, nil, 0, and "" are falsey +func Compact[T any](slice []T) []T { + res := make([]T, 0, 0) + for _, v := range slice { + if !reflect.DeepEqual(v, nil) && + !reflect.DeepEqual(v, false) && + !reflect.DeepEqual(v, "") && + !reflect.DeepEqual(v, 0) { res = append(res, v) } } @@ -83,6 +86,46 @@ func Difference[T comparable](slice1, slice2 []T) []T { return res } +// Concat creates a new slice concatenating slice with any additional slices and/or values. +func Concat[T any](slice []T, values ...[]T) []T { + res := append([]T{}, slice...) + + for _, v := range values { + res = append(res, v...) + } + + return res +} + +// Difference creates an slice of whose element in slice but not in comparedSlice +func Difference[T comparable](slice, comparedSlice []T) []T { + var res []T + for _, v := range slice { + if !Contain(comparedSlice, v) { + res = append(res, v) + } + } + + return res +} + +// DifferenceBy it accepts iteratee which is invoked for each element of slice +// and values to generate the criterion by which they're compared. +// like lodash.js differenceBy: https://lodash.com/docs/4.17.15#differenceBy, +func DifferenceBy[T any](slice []T, comparedSlice []T, iteratee func(index int, t T) T) []T { + orginSliceAfterMap := Map(slice, iteratee) + comparedSliceAfterMap := Map(comparedSlice, iteratee) + + res := make([]T, 0, 0) + for i, v := range orginSliceAfterMap { + if !Contain(comparedSliceAfterMap, v) { + res = append(res, slice[i]) + } + } + + return res +} + // Every return true if all of the values in the slice pass the predicate function. // The fn function signature should be func(int, T) bool . func Every[T any](slice []T, fn func(index int, t T) bool) bool { diff --git a/slice/slice_test.go b/slice/slice_test.go index 66aab40..d914b34 100644 --- a/slice/slice_test.go +++ b/slice/slice_test.go @@ -29,6 +29,7 @@ func TestChunk(t *testing.T) { assert := internal.NewAssert(t, "TestChunk") arr := []string{"a", "b", "c", "d", "e"} + r1 := [][]string{{"a"}, {"b"}, {"c"}, {"d"}, {"e"}} assert.Equal(r1, Chunk(arr, 1)) @@ -45,6 +46,27 @@ func TestChunk(t *testing.T) { assert.Equal(r5, Chunk(arr, 5)) } +func TestCompact(t *testing.T) { + assert := internal.NewAssert(t, "TesCompact") + + assert.Equal([]int{}, Compact([]int{0})) + assert.Equal([]int{1, 2, 3}, Compact([]int{0, 1, 2, 3})) + assert.Equal([]string{}, Compact([]string{""})) + assert.Equal([]string{" "}, Compact([]string{" "})) + assert.Equal([]string{"a", "b", "0"}, Compact([]string{"", "a", "b", "0"})) + assert.Equal([]bool{true, true}, Compact([]bool{false, true, true})) +} + +func TestConcat(t *testing.T) { + assert := internal.NewAssert(t, "Concat") + + // assert.Equal([]int{0}, Concat([]int{}, 0)) + // assert.Equal([]int{1, 2, 3, 4, 5}, Concat([]int{1, 2, 3}, 4, 5)) + assert.Equal([]int{1, 2, 3, 4, 5}, Concat([]int{1, 2, 3}, []int{4, 5})) + assert.Equal([]int{1, 2, 3, 4, 5}, Concat([]int{1, 2, 3}, []int{4}, []int{5})) + // assert.Equal([]int{1, 2, 3, 4, 5}, Concat([]int{1, 2, 3}, []int{4}, 5)) +} + func TestEvery(t *testing.T) { nums := []int{1, 2, 3, 5} isEven := func(i, num int) bool { @@ -377,6 +399,17 @@ func TestDifference(t *testing.T) { assert.Equal([]int{1, 2, 3}, Difference(s1, s2)) } +func TestDifferenceBy(t *testing.T) { + assert := internal.NewAssert(t, "TestDifferenceBy") + + s1 := []int{1, 2, 3, 4, 5} //after add one: 2 3 4 5 6 + s2 := []int{3, 4, 5} //after add one: 4 5 6 + addOne := func(i int, v int) int { + return v + 1 + } + assert.Equal([]int{1, 2}, DifferenceBy(s1, s2, addOne)) +} + func TestSortByFielDesc(t *testing.T) { assert := internal.NewAssert(t, "TestWithout") diff --git a/strutil/string.go b/strutil/string.go index f3b1094..268099d 100644 --- a/strutil/string.go +++ b/strutil/string.go @@ -54,6 +54,18 @@ func Capitalize(s string) string { return string(out) } +// UpperFirst converts the first character of string to upper case. +func UpperFirst(s string) string { + if len(s) == 0 { + return "" + } + + r, size := utf8.DecodeRuneInString(s) + r = unicode.ToUpper(r) + + return string(r) + s[size:] +} + // LowerFirst converts the first character of string to lower case. func LowerFirst(s string) string { if len(s) == 0 { diff --git a/strutil/string_test.go b/strutil/string_test.go index ac7e35c..16432b7 100644 --- a/strutil/string_test.go +++ b/strutil/string_test.go @@ -50,6 +50,17 @@ func TestSnakeCase(t *testing.T) { assert.NotEqual("foo-bar", SnakeCase("foo_Bar")) } +func TestUpperFirst(t *testing.T) { + assert := internal.NewAssert(t, "TestLowerFirst") + + assert.Equal("Foo", UpperFirst("foo")) + assert.Equal("BAR", UpperFirst("bAR")) + assert.Equal("FOo", UpperFirst("FOo")) + assert.Equal("FOo大", UpperFirst("fOo大")) + + assert.NotEqual("Bar", UpperFirst("BAR")) +} + func TestLowerFirst(t *testing.T) { assert := internal.NewAssert(t, "TestLowerFirst") diff --git a/system/os.go b/system/os.go index f9a290e..91a1cb9 100644 --- a/system/os.go +++ b/system/os.go @@ -56,6 +56,9 @@ func ExecCommand(command string) (stdout, stderr string, err error) { var errout bytes.Buffer cmd := exec.Command("/bin/bash", "-c", command) + if IsWindows() { + cmd = exec.Command("cmd") + } cmd.Stdout = &out cmd.Stderr = &errout err = cmd.Run() diff --git a/system/os_test.go b/system/os_test.go index 1d5ac1f..d70813f 100644 --- a/system/os_test.go +++ b/system/os_test.go @@ -56,5 +56,7 @@ func TestExecCommand(t *testing.T) { t.Logf("error: %v\n", err) } - assert.IsNotNil(err) + if !IsWindows() { + assert.IsNotNil(err) + } } diff --git a/validator/validator.go b/validator/validator.go index bb1b8bd..362d8ec 100644 --- a/validator/validator.go +++ b/validator/validator.go @@ -7,6 +7,7 @@ package validator import ( "encoding/json" "net" + "net/url" "regexp" "strconv" "strings" @@ -123,6 +124,27 @@ func IsPort(str string) bool { return false } +var isUrlRegexMatcher *regexp.Regexp = regexp.MustCompile(`^((ftp|http|https?):\/\/)?(\S+(:\S*)?@)?((([1-9]\d?|1\d\d|2[01]\d|22[0-3])(\.(1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(([a-zA-Z0-9]+([-\.][a-zA-Z0-9]+)*)|((www\.)?))?(([a-z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-z\x{00a1}-\x{ffff}]{2,}))?))(:(\d{1,5}))?((\/|\?|#)[^\s]*)?$`) + +// IsUrl check if the string is url. +func IsUrl(str string) bool { + if str == "" || len(str) >= 2083 || len(str) <= 3 || strings.HasPrefix(str, ".") { + return false + } + u, err := url.Parse(str) + if err != nil { + return false + } + if strings.HasPrefix(u.Host, ".") { + return false + } + if u.Host == "" && (u.Path != "" && !strings.Contains(u.Path, ".")) { + return false + } + + return isUrlRegexMatcher.MatchString(str) +} + var isDnsRegexMatcher *regexp.Regexp = regexp.MustCompile(`^[a-zA-Z]([a-zA-Z0-9\-]+[\.]?)*[a-zA-Z0-9]$`) // IsDns check if the string is dns. diff --git a/validator/validator_test.go b/validator/validator_test.go index 6d07bb6..f9f41f9 100644 --- a/validator/validator_test.go +++ b/validator/validator_test.go @@ -160,11 +160,21 @@ func TestIsIpV6(t *testing.T) { assert.Equal(true, IsIpV6("::0:0:0:0:0:0:1")) } +func TestIsUrl(t *testing.T) { + assert := internal.NewAssert(t, "TestIsUrl") + + assert.Equal(true, IsUrl("http://abc.com")) + assert.Equal(true, IsUrl("abc.com")) + assert.Equal(true, IsUrl("a.b.com")) + assert.Equal(false, IsUrl("abc")) +} + func TestIsDns(t *testing.T) { assert := internal.NewAssert(t, "TestIsDns") assert.Equal(true, IsDns("abc.com")) assert.Equal(false, IsDns("a.b.com")) + assert.Equal(false, IsDns("http://abc.com")) } func TestIsEmail(t *testing.T) {