25
.github/workflows/go.yml
vendored
Normal file
25
.github/workflows/go.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: Go
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: 1.18
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -v ./...
|
||||||
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
# *.out
|
||||||
|
|
||||||
|
# Test coverage output
|
||||||
|
coverage*.*
|
||||||
|
|
||||||
|
# postgres data volume used by postgres server container for testing purpose
|
||||||
|
testdata/postgres
|
||||||
|
|
||||||
|
.idea/
|
||||||
29
LICENSE.md
Normal file
29
LICENSE.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2022, hiscaler
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
179
README.md
Normal file
179
README.md
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
gox
|
||||||
|
===
|
||||||
|
|
||||||
|
Golang functions library
|
||||||
|
|
||||||
|
- archivex
|
||||||
|
- zipx
|
||||||
|
- Compress
|
||||||
|
- UnCompress
|
||||||
|
- bytex
|
||||||
|
- IsEmpty
|
||||||
|
- IsBlank
|
||||||
|
- ToString
|
||||||
|
- StartsWith
|
||||||
|
- EndsWith
|
||||||
|
- Contains
|
||||||
|
- cryptox
|
||||||
|
- Crc32
|
||||||
|
- Md5
|
||||||
|
- Sha1
|
||||||
|
- extractx
|
||||||
|
- Number
|
||||||
|
- Numbers
|
||||||
|
- Float64
|
||||||
|
- Float32
|
||||||
|
- Int64
|
||||||
|
- Int32
|
||||||
|
- Int16
|
||||||
|
- Int8
|
||||||
|
- Int
|
||||||
|
- filepathx
|
||||||
|
- Dirs
|
||||||
|
- Files
|
||||||
|
- GenerateDirNames
|
||||||
|
- Ext
|
||||||
|
- filex
|
||||||
|
- IsDir
|
||||||
|
- IsFile
|
||||||
|
- Exists
|
||||||
|
- Size
|
||||||
|
- fmtx
|
||||||
|
- SprettyPrint
|
||||||
|
- PrettyPrint
|
||||||
|
- PrettyPrintln
|
||||||
|
- htmlx
|
||||||
|
- Strip
|
||||||
|
- Spaceless
|
||||||
|
- Clean
|
||||||
|
- Tag
|
||||||
|
- inx
|
||||||
|
- In
|
||||||
|
- StringIn
|
||||||
|
- IntIn
|
||||||
|
- ipx
|
||||||
|
- RemoteAddr
|
||||||
|
- LocalAddr
|
||||||
|
- IsPrivate
|
||||||
|
- IsPublic
|
||||||
|
- Number
|
||||||
|
- Random
|
||||||
|
- String
|
||||||
|
- isx
|
||||||
|
- Number
|
||||||
|
- Empty
|
||||||
|
- Equal
|
||||||
|
- SafeCharacters
|
||||||
|
- HttpURL
|
||||||
|
- OS
|
||||||
|
- ColorHex
|
||||||
|
- jsonx
|
||||||
|
- ToRawMessage
|
||||||
|
- ToJson
|
||||||
|
- ToPrettyJson
|
||||||
|
- EmptyObjectRawMessage
|
||||||
|
- EmptyArrayRawMessage
|
||||||
|
- IsEmptyRawMessage
|
||||||
|
- NewParser
|
||||||
|
- Exists
|
||||||
|
- Find
|
||||||
|
- Interface
|
||||||
|
- String
|
||||||
|
- Int
|
||||||
|
- Int64
|
||||||
|
- Float32
|
||||||
|
- Float64
|
||||||
|
- Bool
|
||||||
|
- keyx
|
||||||
|
- Generate
|
||||||
|
- map
|
||||||
|
- Keys
|
||||||
|
- StringMapStringEncode
|
||||||
|
- net
|
||||||
|
- urlx
|
||||||
|
- NewURL
|
||||||
|
- GetValue
|
||||||
|
- SetValue
|
||||||
|
- AddValue
|
||||||
|
- DelKey
|
||||||
|
- HasKey
|
||||||
|
- String
|
||||||
|
- IsAbsolute
|
||||||
|
- IsRelative
|
||||||
|
- nullx
|
||||||
|
- StringFrom
|
||||||
|
- NullString
|
||||||
|
- TimeFrom
|
||||||
|
- NullTime
|
||||||
|
- pathx
|
||||||
|
- FilenameWithoutExt
|
||||||
|
- randx
|
||||||
|
- Letter
|
||||||
|
- Number
|
||||||
|
- Any
|
||||||
|
- setx
|
||||||
|
- ToSet
|
||||||
|
- ToStringSet
|
||||||
|
- ToIntSet
|
||||||
|
- slicex
|
||||||
|
- Map
|
||||||
|
- Filter
|
||||||
|
- ToInterface
|
||||||
|
- StringToInterface
|
||||||
|
- IntToInterface
|
||||||
|
- StringSliceEqual
|
||||||
|
- IntSliceEqual
|
||||||
|
- StringSliceReverse
|
||||||
|
- IntSliceReverse
|
||||||
|
- Diff
|
||||||
|
- StringSliceDiff
|
||||||
|
- IntSliceDiff
|
||||||
|
- Chunk
|
||||||
|
- spreedsheetx
|
||||||
|
- NewColumn()
|
||||||
|
```go
|
||||||
|
column := NewColumn("A")
|
||||||
|
column.Next() // Return `B` if successful
|
||||||
|
column.RightShift(26) // Return `AB` if successful
|
||||||
|
column.LeftShift(1) // Return `AA` if successful
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
- stringx
|
||||||
|
- IsEmpty
|
||||||
|
- IsBlank
|
||||||
|
- ToNumber
|
||||||
|
- ContainsChinese
|
||||||
|
- ToNarrow
|
||||||
|
- ToWiden
|
||||||
|
- Split
|
||||||
|
- String
|
||||||
|
- RemoveEmoji
|
||||||
|
- TrimAny
|
||||||
|
- RemoveExtraSpace
|
||||||
|
- SequentialWordFields
|
||||||
|
- ToBytes
|
||||||
|
- WordMatched
|
||||||
|
- StartsWith
|
||||||
|
- EndsWith
|
||||||
|
- Contains
|
||||||
|
- QuoteMeta
|
||||||
|
- HexToByte
|
||||||
|
- Len
|
||||||
|
- UpperFirst
|
||||||
|
- LowerFirst
|
||||||
|
- timex
|
||||||
|
- IsAmericaSummerTime
|
||||||
|
- ChineseTimeLocation
|
||||||
|
- Between
|
||||||
|
- DayStart
|
||||||
|
- DayEnd
|
||||||
|
- MonthStart
|
||||||
|
- MonthEnd
|
||||||
|
- IsAM
|
||||||
|
- IsPM
|
||||||
|
- WeekStart
|
||||||
|
- WeekEnd
|
||||||
|
- YearWeeksByWeek
|
||||||
|
- YearWeeksByTime
|
||||||
|
- XISOWeek
|
||||||
1024
archivex/zipx/testdata/a/a.txt
vendored
Normal file
1024
archivex/zipx/testdata/a/a.txt
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
archivex/zipx/testdata/b/b.txt
vendored
Normal file
1
archivex/zipx/testdata/b/b.txt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
b file content
|
||||||
2
archivex/zipx/testdata/中国/你好.txt
vendored
Normal file
2
archivex/zipx/testdata/中国/你好.txt
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Hello, China!
|
||||||
|
你好,中国!
|
||||||
163
archivex/zipx/zip.go
Normal file
163
archivex/zipx/zip.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package zipx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.cloudyne.io/go/hiscaler-gox/filex"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
type zipFile struct {
|
||||||
|
header *zip.FileHeader
|
||||||
|
data *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compress compresses files and saved, if compactDirectory is true, then will remove all directory path
|
||||||
|
func Compress(filename string, files []string, method uint16, compactDirectory bool) error {
|
||||||
|
zFile, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer zFile.Close()
|
||||||
|
zipWriter := zip.NewWriter(zFile)
|
||||||
|
defer zipWriter.Close()
|
||||||
|
|
||||||
|
zipFiles := make([]zipFile, len(files))
|
||||||
|
errGrp, ctx := errgroup.WithContext(context.Background())
|
||||||
|
for i, file := range files {
|
||||||
|
f := file
|
||||||
|
j := i
|
||||||
|
errGrp.Go(func() error {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
zf, e := addFile(f, method, compactDirectory)
|
||||||
|
if e != nil {
|
||||||
|
ctx.Done()
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
zipFiles[j] = zf
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
err = errGrp.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range zipFiles {
|
||||||
|
if zipFiles[i].data == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = func(i int) error {
|
||||||
|
defer zipFiles[i].data.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err // For close all opened files
|
||||||
|
}
|
||||||
|
writer, e := zipWriter.CreateHeader(zipFiles[i].header)
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
_, e = io.Copy(writer, zipFiles[i].data)
|
||||||
|
return e
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func addFile(filename string, method uint16, compactDirectory bool) (zipFile zipFile, err error) {
|
||||||
|
pendingAddFile, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer pendingAddFile.Close()
|
||||||
|
|
||||||
|
zipFile.data = pendingAddFile
|
||||||
|
info, err := pendingAddFile.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := zip.FileInfoHeader(info)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if compactDirectory {
|
||||||
|
header.Name = filepath.Base(filename)
|
||||||
|
} else {
|
||||||
|
header.Name = filename
|
||||||
|
}
|
||||||
|
header.Method = method
|
||||||
|
zipFile.header = header
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnCompress unzip source file to destination directory
|
||||||
|
func UnCompress(src, dst string) error {
|
||||||
|
r, err := zip.OpenReader(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
// Create destination directory if not exists
|
||||||
|
if !filex.Exists(dst) {
|
||||||
|
err = os.MkdirAll(dst, fs.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range r.File {
|
||||||
|
path := filepath.Join(dst, file.Name)
|
||||||
|
if file.FileInfo().IsDir() {
|
||||||
|
if err = os.MkdirAll(path, file.Mode()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
if !filex.Exists(dir) {
|
||||||
|
err = os.MkdirAll(dir, fs.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = writeFile(file, path); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFile(file *zip.File, path string) error {
|
||||||
|
fr, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer fr.Close()
|
||||||
|
fw, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fw.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(fw, fr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
68
archivex/zipx/zip_test.go
Normal file
68
archivex/zipx/zip_test.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package zipx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.cloudyne.io/go/hiscaler-gox/filex"
|
||||||
|
)
|
||||||
|
|
||||||
|
var files []string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
files = []string{
|
||||||
|
"./zip.go",
|
||||||
|
"./testdata/a/a.txt",
|
||||||
|
"./testdata/b/b.txt",
|
||||||
|
"./testdata/中国/你好.txt",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompressCompactDirectory(t *testing.T) {
|
||||||
|
err := Compress("./a.zip", files, zip.Deflate, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if !filex.Exists("./a.zip") {
|
||||||
|
t.Error("zip file not exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompressUnCompactDirectory(t *testing.T) {
|
||||||
|
err := Compress("./a.zip", files, zip.Deflate, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if !filex.Exists("./a.zip") {
|
||||||
|
t.Error("zip file not exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompressError(t *testing.T) {
|
||||||
|
notExistsFiles := make([]string, 0)
|
||||||
|
for i := 0; i <= 100; i++ {
|
||||||
|
notExistsFiles = append(notExistsFiles, fmt.Sprintf("%d-not-exists.file", i))
|
||||||
|
}
|
||||||
|
err := Compress("./a.zip", notExistsFiles, zip.Deflate, true)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("err is nil")
|
||||||
|
} else {
|
||||||
|
t.Logf("err = %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnCompress(t *testing.T) {
|
||||||
|
TestCompressUnCompactDirectory(t)
|
||||||
|
err := UnCompress("./a.zip", "./a")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
} else {
|
||||||
|
for _, file := range files {
|
||||||
|
checkFile := filepath.Join("./a", file)
|
||||||
|
if !filex.Exists(checkFile) {
|
||||||
|
t.Errorf("%s is not exists", checkFile)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
bytex/byte.go
Normal file
90
bytex/byte.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package bytex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsEmpty Check byte is empty
|
||||||
|
func IsEmpty(b []byte) bool {
|
||||||
|
return len(b) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsBlank(b []byte) bool {
|
||||||
|
return len(b) == 0 || len(bytes.TrimSpace(b)) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToString(b []byte) string {
|
||||||
|
return *(*string)(unsafe.Pointer(&b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartsWith(s []byte, ss [][]byte, caseSensitive bool) bool {
|
||||||
|
if ss == nil || len(ss) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
has := false
|
||||||
|
if !caseSensitive {
|
||||||
|
s = bytes.ToLower(s)
|
||||||
|
}
|
||||||
|
for _, prefix := range ss {
|
||||||
|
if len(prefix) == 0 {
|
||||||
|
has = true
|
||||||
|
} else {
|
||||||
|
if !caseSensitive {
|
||||||
|
prefix = bytes.ToLower(prefix)
|
||||||
|
}
|
||||||
|
has = bytes.HasPrefix(s, prefix)
|
||||||
|
}
|
||||||
|
if has {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return has
|
||||||
|
}
|
||||||
|
|
||||||
|
func EndsWith(s []byte, ss [][]byte, caseSensitive bool) bool {
|
||||||
|
if ss == nil || len(ss) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
has := false
|
||||||
|
if !caseSensitive {
|
||||||
|
s = bytes.ToLower(s)
|
||||||
|
}
|
||||||
|
for _, suffix := range ss {
|
||||||
|
if len(suffix) == 0 {
|
||||||
|
has = true
|
||||||
|
} else {
|
||||||
|
if !caseSensitive {
|
||||||
|
suffix = bytes.ToLower(suffix)
|
||||||
|
}
|
||||||
|
has = bytes.HasSuffix(s, suffix)
|
||||||
|
}
|
||||||
|
if has {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return has
|
||||||
|
}
|
||||||
|
|
||||||
|
func Contains(s []byte, ss [][]byte, caseSensitive bool) bool {
|
||||||
|
in := false
|
||||||
|
if !caseSensitive {
|
||||||
|
s = bytes.ToLower(s)
|
||||||
|
}
|
||||||
|
for _, substr := range ss {
|
||||||
|
if len(substr) == 0 {
|
||||||
|
in = true
|
||||||
|
} else {
|
||||||
|
if !caseSensitive {
|
||||||
|
substr = bytes.ToLower(substr)
|
||||||
|
}
|
||||||
|
in = bytes.Contains(s, substr)
|
||||||
|
}
|
||||||
|
if in {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return in
|
||||||
|
}
|
||||||
126
bytex/byte_test.go
Normal file
126
bytex/byte_test.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package bytex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsEmpty(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
Number int
|
||||||
|
Byte []byte
|
||||||
|
Except bool
|
||||||
|
}{
|
||||||
|
{1, []byte("a"), false},
|
||||||
|
{2, []byte(""), true},
|
||||||
|
{3, []byte(" "), false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
isEmpty := IsEmpty(testCase.Byte)
|
||||||
|
if isEmpty != testCase.Except {
|
||||||
|
t.Errorf("%d except: %#v, actual: %#v", testCase.Number, testCase.Except, isEmpty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsBlank(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
Number int
|
||||||
|
Byte []byte
|
||||||
|
Except bool
|
||||||
|
}{
|
||||||
|
{1, []byte("a"), false},
|
||||||
|
{2, []byte(""), true},
|
||||||
|
{3, []byte(" "), true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
isBlank := IsBlank(testCase.Byte)
|
||||||
|
if isBlank != testCase.Except {
|
||||||
|
t.Errorf("%d except: %#v, actual: %#v", testCase.Number, testCase.Except, isBlank)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToString(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
bytesValue []byte
|
||||||
|
string string
|
||||||
|
}{
|
||||||
|
{"t1", []byte{'a'}, "a"},
|
||||||
|
{"t2", []byte("abc"), "abc"},
|
||||||
|
{"t3", []byte("a b c "), "a b c "},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
s := ToString(test.bytesValue)
|
||||||
|
assert.Equal(t, test.string, s, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartsWith(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
string []byte
|
||||||
|
words [][]byte
|
||||||
|
caseSensitive bool
|
||||||
|
except bool
|
||||||
|
}{
|
||||||
|
{"t1", []byte("Hello world!"), [][]byte{[]byte("he"), []byte("He")}, false, true},
|
||||||
|
{"t2", []byte("Hello world!"), [][]byte{[]byte("he"), []byte("He")}, true, true},
|
||||||
|
{"t3", []byte("Hello world!"), [][]byte{[]byte("he")}, true, false},
|
||||||
|
{"t4", []byte(""), [][]byte{[]byte("")}, true, true},
|
||||||
|
{"t5", []byte(""), nil, true, true},
|
||||||
|
{"t6", []byte(""), [][]byte{}, true, true},
|
||||||
|
{"t7", []byte("Hello world!"), [][]byte{[]byte("")}, true, true},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
b := StartsWith(test.string, test.words, test.caseSensitive)
|
||||||
|
assert.Equal(t, test.except, b, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEndsWith(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
string []byte
|
||||||
|
words [][]byte
|
||||||
|
caseSensitive bool
|
||||||
|
except bool
|
||||||
|
}{
|
||||||
|
{"t1", []byte("Hello world!"), [][]byte{[]byte("he"), []byte("He")}, false, false},
|
||||||
|
{"t2", []byte("Hello world!"), [][]byte{[]byte("he"), []byte("He")}, true, false},
|
||||||
|
{"t3", []byte("Hello world!"), [][]byte{[]byte("d!"), []byte("!")}, true, true},
|
||||||
|
{"t4", []byte("Hello world!"), [][]byte{[]byte("WORLD!")}, false, true},
|
||||||
|
{"t5", []byte(""), [][]byte{[]byte("")}, true, true},
|
||||||
|
{"t6", []byte(""), nil, true, true},
|
||||||
|
{"t7", []byte(""), [][]byte{}, true, true},
|
||||||
|
{"t8", []byte("Hello world!"), [][]byte{[]byte("")}, true, true},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
b := EndsWith(test.string, test.words, test.caseSensitive)
|
||||||
|
assert.Equal(t, test.except, b, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContains(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
string []byte
|
||||||
|
words [][]byte
|
||||||
|
caseSensitive bool
|
||||||
|
except bool
|
||||||
|
}{
|
||||||
|
{"t1", []byte("Hello world!"), [][]byte{[]byte("ol"), []byte("LL")}, false, true},
|
||||||
|
{"t2", []byte("Hello world!"), [][]byte{[]byte("ol"), []byte("LL")}, true, false},
|
||||||
|
{"t3", []byte("Hello world!"), [][]byte{[]byte("notfound"), []byte("world")}, false, true},
|
||||||
|
{"t4", []byte("Hello world!"), [][]byte{[]byte("notfound"), []byte("world")}, true, true},
|
||||||
|
{"t5", []byte(""), [][]byte{[]byte("")}, true, true},
|
||||||
|
{"t6", []byte("Hello world!"), [][]byte{[]byte("")}, true, true},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
b := Contains(test.string, test.words, test.caseSensitive)
|
||||||
|
assert.Equal(t, test.except, b, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
cryptox/crc32.go
Normal file
7
cryptox/crc32.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package cryptox
|
||||||
|
|
||||||
|
import "hash/crc32"
|
||||||
|
|
||||||
|
func Crc32(s string) uint32 {
|
||||||
|
return crc32.ChecksumIEEE([]byte(s))
|
||||||
|
}
|
||||||
12
cryptox/md5.go
Normal file
12
cryptox/md5.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package cryptox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Md5(s string) string {
|
||||||
|
h := md5.New()
|
||||||
|
h.Write([]byte(s))
|
||||||
|
return hex.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
12
cryptox/sha1.go
Normal file
12
cryptox/sha1.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package cryptox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Sha1(s string) string {
|
||||||
|
h := sha1.New()
|
||||||
|
h.Write([]byte(s))
|
||||||
|
return hex.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
110
extractx/number.go
Normal file
110
extractx/number.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package extractx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rxNumber = regexp.MustCompile(`\-?\d+[\d.,]*\d*`)
|
||||||
|
|
||||||
|
// 提取的内容默认为 1,234.56 格式的数字,未实现根据国家标准实现提取
|
||||||
|
// https://zhuanlan.zhihu.com/p/157980325
|
||||||
|
func clean(s string) string {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
s = strings.ReplaceAll(s, ",", "")
|
||||||
|
n := len(s)
|
||||||
|
if s[n-1:] == "." {
|
||||||
|
s = s[n-2 : n-1]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func Number(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return clean(rxNumber.FindString(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Numbers(s string) []string {
|
||||||
|
if s == "" {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := rxNumber.FindAllString(s, -1)
|
||||||
|
if matches == nil {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range matches {
|
||||||
|
matches[i] = clean(v)
|
||||||
|
}
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
func Float64(s string) float64 {
|
||||||
|
if s = Number(s); s != "" {
|
||||||
|
if v, err := strconv.ParseFloat(s, 64); err == nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func Float32(s string) float32 {
|
||||||
|
if s = Number(s); s != "" {
|
||||||
|
if v, err := strconv.ParseFloat(s, 64); err == nil {
|
||||||
|
return float32(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func Int64(s string) int64 {
|
||||||
|
if s = Number(s); s != "" {
|
||||||
|
if v, err := strconv.ParseInt(s, 10, 64); err == nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func Int32(s string) int32 {
|
||||||
|
if s = Number(s); s != "" {
|
||||||
|
if v, err := strconv.ParseInt(s, 10, 32); err == nil {
|
||||||
|
return int32(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func Int16(s string) int16 {
|
||||||
|
if s = Number(s); s != "" {
|
||||||
|
if v, err := strconv.ParseInt(s, 10, 16); err == nil {
|
||||||
|
return int16(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func Int8(s string) int8 {
|
||||||
|
if s = Number(s); s != "" {
|
||||||
|
if v, err := strconv.ParseInt(s, 10, 16); err == nil {
|
||||||
|
return int8(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func Int(s string) int {
|
||||||
|
if s = Number(s); s != "" {
|
||||||
|
if v, err := strconv.ParseInt(s, 10, 16); err == nil {
|
||||||
|
return int(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
59
extractx/number_test.go
Normal file
59
extractx/number_test.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package extractx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNumber(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
string string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t1", "123", "123"},
|
||||||
|
{"t2", "12.3", "12.3"},
|
||||||
|
{"t3", "1,234.3", "1234.3"},
|
||||||
|
{"t4", " ab 1 123", "1"},
|
||||||
|
{"t5", ".", ""},
|
||||||
|
{"t6", ",", ""},
|
||||||
|
{"t7", ".,", ""},
|
||||||
|
{"t8", "100 23.", "100"},
|
||||||
|
{"t8.1", "$100 $23.", "100"},
|
||||||
|
{"t9", "-1", "-1"},
|
||||||
|
{"t10", "-1-1", "-1"}, // todo maybe is empty
|
||||||
|
{"t11", "+1", "1"},
|
||||||
|
{"t12", "+1+1", "1"}, // todo maybe is empty
|
||||||
|
{"t13", "1.0 out of 5 stars", "1.0"},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
n := Number(testCase.string)
|
||||||
|
assert.Equal(t, testCase.expected, n, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNumbers(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
string string
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{"t1", "123", []string{"123"}},
|
||||||
|
{"t2", "12.3", []string{"12.3"}},
|
||||||
|
{"t3", "1,234.3", []string{"1234.3"}},
|
||||||
|
{"t4", " ab 1 123", []string{"1", "123"}},
|
||||||
|
{"t5", " ab .1 123", []string{"1", "123"}},
|
||||||
|
{"t5", " ab ,1 123", []string{"1", "123"}},
|
||||||
|
{"t6", " ab 1. 123", []string{"1", "123"}},
|
||||||
|
{"t7", "$100,$200", []string{"100", "200"}},
|
||||||
|
{"t8", "1,2.3,4", []string{"12.34"}},
|
||||||
|
{"t9", "1, 2.3, 4", []string{"1", "2.3", "4"}},
|
||||||
|
{"t10", "-123,4", []string{"-1234"}},
|
||||||
|
{"t11", "1-1", []string{"1", "-1"}}, // todo May be return empty string
|
||||||
|
{"t12", "N1-1", []string{"1", "-1"}}, // todo May be return empty string
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
n := Numbers(testCase.string)
|
||||||
|
assert.Equal(t, testCase.expected, n, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
290
filepathx/filepath.go
Normal file
290
filepathx/filepath.go
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
package filepathx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.cloudyne.io/go/hiscaler-gox/filex"
|
||||||
|
"git.cloudyne.io/go/hiscaler-gox/inx"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
searchDir = iota
|
||||||
|
searchFile
|
||||||
|
)
|
||||||
|
|
||||||
|
type WalkOption struct {
|
||||||
|
FilterFunc func(path string) bool // 自定义函数,返回 true 则会加到列表中,否则忽略。当定义该函数时,将会忽略掉 Except, Only 设置
|
||||||
|
Except []string // 排除的文件或者目录(仅当 FilterFunc 未设置时起作用)
|
||||||
|
Only []string // 仅仅符合列表中的文件或者目录才会返回(仅当 FilterFunc 未设置时起作用)
|
||||||
|
CaseSensitive bool // 区分大小写(作用于 Except 和 Only 设置)
|
||||||
|
Recursive bool // 是否递归查询下级目录
|
||||||
|
}
|
||||||
|
|
||||||
|
func read(root string, recursive bool, searchType int) []string {
|
||||||
|
dfs := os.DirFS(root)
|
||||||
|
paths := make([]string, 0)
|
||||||
|
if recursive {
|
||||||
|
fs.WalkDir(dfs, ".", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err == nil && path != "." && path != ".." &&
|
||||||
|
((searchType == searchDir && d.IsDir()) || (searchType == searchFile && !d.IsDir())) {
|
||||||
|
paths = append(paths, filepath.Join(root, path))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ds, err := fs.ReadDir(dfs, ".")
|
||||||
|
if err == nil {
|
||||||
|
for _, d := range ds {
|
||||||
|
if d.Name() != "." && d.Name() != ".." &&
|
||||||
|
((searchType == searchDir && d.IsDir()) || (searchType == searchFile && !d.IsDir())) {
|
||||||
|
paths = append(paths, filepath.Join(root, d.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pathPrefix := ""
|
||||||
|
if strings.HasPrefix(root, "..") {
|
||||||
|
pathPrefix = ".."
|
||||||
|
} else if strings.HasPrefix(root, ".") {
|
||||||
|
pathPrefix = "."
|
||||||
|
}
|
||||||
|
if pathPrefix != "" {
|
||||||
|
pathPrefix += string(filepath.Separator)
|
||||||
|
for i, path := range paths {
|
||||||
|
paths[i] = pathPrefix + path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterPath(path string, opt WalkOption) (ok bool) {
|
||||||
|
if (opt.FilterFunc == nil && len(opt.Only) == 0 && len(opt.Except) == 0) ||
|
||||||
|
(opt.FilterFunc != nil && opt.FilterFunc(path)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opt.Except) > 0 || len(opt.Only) > 0 {
|
||||||
|
name := filepath.Base(path)
|
||||||
|
if len(opt.Except) > 0 {
|
||||||
|
if opt.CaseSensitive {
|
||||||
|
ok = true
|
||||||
|
for _, s := range opt.Except {
|
||||||
|
if s == name {
|
||||||
|
ok = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ok = !inx.StringIn(name, opt.Except...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(opt.Only) > 0 {
|
||||||
|
if opt.CaseSensitive {
|
||||||
|
for _, s := range opt.Only {
|
||||||
|
if s == name {
|
||||||
|
ok = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ok = inx.StringIn(name, opt.Only...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dirs 获取指定目录下的所有目录
|
||||||
|
func Dirs(root string, opt WalkOption) []string {
|
||||||
|
dirs := make([]string, 0)
|
||||||
|
paths := read(root, opt.Recursive, searchDir)
|
||||||
|
if len(paths) > 0 {
|
||||||
|
for _, path := range paths {
|
||||||
|
if filterPath(path, opt) && !strings.EqualFold(path, root) {
|
||||||
|
dirs = append(dirs, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files 获取指定目录下的所有文件
|
||||||
|
func Files(root string, opt WalkOption) []string {
|
||||||
|
files := make([]string, 0)
|
||||||
|
paths := read(root, opt.Recursive, searchFile)
|
||||||
|
if len(paths) > 0 {
|
||||||
|
for _, path := range paths {
|
||||||
|
if filterPath(path, opt) {
|
||||||
|
files = append(files, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateDirNames 生成目录名
|
||||||
|
func GenerateDirNames(s string, n, level int, caseSensitive bool) []string {
|
||||||
|
if s == "" {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
isValidCharFunc := func(r rune) bool {
|
||||||
|
return 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' || '0' <= r && r <= '9'
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
for _, r := range s {
|
||||||
|
if isValidCharFunc(r) {
|
||||||
|
b.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if b.Len() == 0 {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
s = b.String() // Clean s string
|
||||||
|
if !caseSensitive {
|
||||||
|
s = strings.ToLower(s)
|
||||||
|
}
|
||||||
|
if n <= 0 {
|
||||||
|
return []string{s}
|
||||||
|
}
|
||||||
|
|
||||||
|
if level <= 0 {
|
||||||
|
level = 1
|
||||||
|
}
|
||||||
|
names := make([]string, 0)
|
||||||
|
sLen := len(s)
|
||||||
|
for i := 0; i < sLen; i += n {
|
||||||
|
if len(names) == level {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex := i + n
|
||||||
|
if lastIndex >= sLen {
|
||||||
|
lastIndex = sLen
|
||||||
|
}
|
||||||
|
names = append(names, s[i:lastIndex])
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ext 获取资源扩展名
|
||||||
|
func Ext(path string, b []byte) string {
|
||||||
|
if path == "" && b == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if b == nil && filex.Exists(path) {
|
||||||
|
if b1, err := os.ReadFile(path); err == nil {
|
||||||
|
b = b1[:512]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ext := ""
|
||||||
|
if b != nil {
|
||||||
|
contentType := http.DetectContentType(b)
|
||||||
|
// https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
||||||
|
extTypes := map[string][]string{
|
||||||
|
".aac": {"audio/aac"},
|
||||||
|
".abw": {"application/x-abiword"},
|
||||||
|
".arc": {"application/x-freearc"},
|
||||||
|
".avi": {"video/x-msvideo"},
|
||||||
|
".azw": {"application/vnd.amazon.ebook"},
|
||||||
|
// ".bin": {"application/octet-stream"},
|
||||||
|
".bmp": {"image/bmp"},
|
||||||
|
".bz": {"application/x-bzip"},
|
||||||
|
".bz2": {"application/x-bzip2"},
|
||||||
|
".csh": {"application/x-csh"},
|
||||||
|
".css": {"text/css"},
|
||||||
|
".csv": {"text/csv"},
|
||||||
|
".doc": {"application/msword"},
|
||||||
|
".docx": {"application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
|
||||||
|
".eot": {"application/vnd.ms-fontobject"},
|
||||||
|
".epub": {"application/epub+zip"},
|
||||||
|
".gif": {"image/gif"},
|
||||||
|
".htm": {"text/html"},
|
||||||
|
".html": {"text/html"},
|
||||||
|
".ico": {"image/vnd.microsoft.icon"},
|
||||||
|
".ics": {"text/calendar"},
|
||||||
|
".jar": {"application/java-archive"},
|
||||||
|
".jpg": {"image/jpeg"},
|
||||||
|
".jpeg": {"image/jpeg"},
|
||||||
|
".js": {"text/javascript"},
|
||||||
|
".json": {"application/json"},
|
||||||
|
".jsonld": {"application/ld+json"},
|
||||||
|
".mid": {"audio/midi", "audio/x-midi"},
|
||||||
|
".midi": {"audio/midi", "audio/x-midi"},
|
||||||
|
".mjs": {"text/javascript"},
|
||||||
|
".mp3": {"audio/mpeg"},
|
||||||
|
".mpeg": {"video/mpeg"},
|
||||||
|
".mpkg": {"application/vnd.apple.installer+xml"},
|
||||||
|
".odp": {"application/vnd.oasis.opendocument.presentation"},
|
||||||
|
".ods": {"application/vnd.oasis.opendocument.spreadsheet"},
|
||||||
|
".odt": {"application/vnd.oasis.opendocument.text"},
|
||||||
|
".oga": {"audio/ogg"},
|
||||||
|
".ogv": {"video/ogg"},
|
||||||
|
".ogx": {"application/ogg"},
|
||||||
|
".otf": {"font/otf"},
|
||||||
|
".png": {"image/png"},
|
||||||
|
".pdf": {"application/pdf"},
|
||||||
|
".ppt": {"application/vnd.ms-powerpoint"},
|
||||||
|
".pptx": {"application/vnd.openxmlformats-officedocument.presentationml.presentation"},
|
||||||
|
".rar": {"application/x-rar-compressed"},
|
||||||
|
".rtf": {"application/rtf"},
|
||||||
|
".sh": {"application/x-sh"},
|
||||||
|
".svg": {"image/svg+xml"},
|
||||||
|
".swf": {"application/x-shockwave-flash"},
|
||||||
|
".tar": {"application/x-tar"},
|
||||||
|
".tif": {"image/tiff"},
|
||||||
|
".tiff": {"image/tiff"},
|
||||||
|
".ttf": {"font/ttf"},
|
||||||
|
".txt": {"text/plain"},
|
||||||
|
".vsd": {"application/vnd.visio"},
|
||||||
|
".wav": {"audio/wav"},
|
||||||
|
".weba": {"audio/webm"},
|
||||||
|
".webm": {"video/webm"},
|
||||||
|
".webp": {"image/webp"},
|
||||||
|
".woff": {"font/woff"},
|
||||||
|
".woff2": {"font/woff2"},
|
||||||
|
".xhtml": {"application/xhtml+xml"},
|
||||||
|
".xls": {"application/vnd.ms-excel"},
|
||||||
|
".xlsx": {"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
|
||||||
|
".xml": {"application/xml", "text/xml"},
|
||||||
|
".xul": {"application/vnd.mozilla.xul+xml"},
|
||||||
|
".zip": {"application/zip"},
|
||||||
|
".3gp": {"video/3gpp", "audio/3gpp"},
|
||||||
|
".3g2": {"video/3gpp2", "audio/3gpp2"},
|
||||||
|
".7z": {"application/x-7z-compressed"},
|
||||||
|
}
|
||||||
|
for k, types := range extTypes {
|
||||||
|
for _, v := range types {
|
||||||
|
mime.AddExtensionType(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if extensions, err := mime.ExtensionsByType(contentType); err == nil && extensions != nil {
|
||||||
|
n := len(extensions)
|
||||||
|
if n == 1 {
|
||||||
|
ext = extensions[0]
|
||||||
|
} else {
|
||||||
|
typeExt := map[string]string{
|
||||||
|
"text/plain; charset=utf-8": ".txt",
|
||||||
|
"image/jpeg": ".jpg",
|
||||||
|
}
|
||||||
|
if v, exists := typeExt[contentType]; exists {
|
||||||
|
ext = v
|
||||||
|
} else {
|
||||||
|
ext = extensions[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ext == "" {
|
||||||
|
ext = filepath.Ext(path)
|
||||||
|
}
|
||||||
|
return ext
|
||||||
|
}
|
||||||
240
filepathx/filepath_test.go
Normal file
240
filepathx/filepath_test.go
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
package filepathx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.cloudyne.io/go/hiscaler-gox/slicex"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDirs(t *testing.T) {
|
||||||
|
root, _ := os.Getwd()
|
||||||
|
testCases := []struct {
|
||||||
|
Number int
|
||||||
|
Path string
|
||||||
|
Option WalkOption
|
||||||
|
Files []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
1,
|
||||||
|
"/a/b",
|
||||||
|
WalkOption{},
|
||||||
|
[]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
2,
|
||||||
|
root,
|
||||||
|
WalkOption{
|
||||||
|
CaseSensitive: false,
|
||||||
|
FilterFunc: func(path string) bool {
|
||||||
|
return filepath.Base(path) == "2"
|
||||||
|
},
|
||||||
|
Recursive: true,
|
||||||
|
},
|
||||||
|
[]string{"2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
3,
|
||||||
|
root,
|
||||||
|
WalkOption{
|
||||||
|
CaseSensitive: false,
|
||||||
|
Only: []string{"2"},
|
||||||
|
Recursive: true,
|
||||||
|
},
|
||||||
|
[]string{"2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
4,
|
||||||
|
root,
|
||||||
|
WalkOption{
|
||||||
|
CaseSensitive: false,
|
||||||
|
Except: []string{"2"},
|
||||||
|
Recursive: true,
|
||||||
|
},
|
||||||
|
[]string{"testdata", "1", "1.1", "1.1", "1.1.1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
5,
|
||||||
|
root,
|
||||||
|
WalkOption{
|
||||||
|
CaseSensitive: false,
|
||||||
|
Recursive: true,
|
||||||
|
},
|
||||||
|
[]string{"testdata", "1", "1.1", "1.1", "2", "1.1.1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
6,
|
||||||
|
root + "/testdata",
|
||||||
|
WalkOption{
|
||||||
|
Recursive: true,
|
||||||
|
},
|
||||||
|
[]string{"1", "1.1", "1.1", "2", "1.1.1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
7,
|
||||||
|
root + "/testdata",
|
||||||
|
WalkOption{
|
||||||
|
Recursive: false,
|
||||||
|
},
|
||||||
|
[]string{"1", "2"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
dirs := Dirs(testCase.Path, testCase.Option)
|
||||||
|
for i, dir := range dirs {
|
||||||
|
dirs[i] = filepath.Base(dir)
|
||||||
|
}
|
||||||
|
if !slicex.StringSliceEqual(dirs, testCase.Files, true, true, true) {
|
||||||
|
t.Errorf("%d: except %v actual %v", testCase.Number, testCase.Files, dirs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFiles(t *testing.T) {
|
||||||
|
root, _ := os.Getwd()
|
||||||
|
testCases := []struct {
|
||||||
|
Number int
|
||||||
|
Path string
|
||||||
|
Option WalkOption
|
||||||
|
Files []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
1,
|
||||||
|
"/a/b",
|
||||||
|
WalkOption{},
|
||||||
|
[]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
2,
|
||||||
|
root,
|
||||||
|
WalkOption{
|
||||||
|
CaseSensitive: false,
|
||||||
|
FilterFunc: func(path string) bool {
|
||||||
|
return filepath.Base(path) == "2.txt"
|
||||||
|
},
|
||||||
|
Recursive: true,
|
||||||
|
},
|
||||||
|
[]string{"2.txt"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
3,
|
||||||
|
root,
|
||||||
|
WalkOption{
|
||||||
|
CaseSensitive: false,
|
||||||
|
Only: []string{"2.txt"},
|
||||||
|
Recursive: true,
|
||||||
|
},
|
||||||
|
[]string{"2.txt"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
4,
|
||||||
|
root,
|
||||||
|
WalkOption{
|
||||||
|
CaseSensitive: false,
|
||||||
|
Except: []string{"2.txt"},
|
||||||
|
Recursive: true,
|
||||||
|
},
|
||||||
|
[]string{"filepath.go", "filepath_test.go", "1.1.txt", "中文_ZH (1).txt", "中文_ZH (1).txt", "中文_ZH (9).txt", "0.txt"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
5,
|
||||||
|
root,
|
||||||
|
WalkOption{
|
||||||
|
CaseSensitive: false,
|
||||||
|
Recursive: true,
|
||||||
|
},
|
||||||
|
[]string{"filepath.go", "filepath_test.go", "1.1.txt", "2.txt", "中文_ZH (1).txt", "中文_ZH (1).txt", "中文_ZH (9).txt", "0.txt"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
6,
|
||||||
|
root + "/testdata",
|
||||||
|
WalkOption{
|
||||||
|
Recursive: true,
|
||||||
|
},
|
||||||
|
[]string{"1.1.txt", "2.txt", "中文_ZH (1).txt", "中文_ZH (1).txt", "中文_ZH (9).txt", "0.txt"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
7,
|
||||||
|
root + "/testdata",
|
||||||
|
WalkOption{
|
||||||
|
Recursive: false,
|
||||||
|
},
|
||||||
|
[]string{"0.txt"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
8,
|
||||||
|
root + "/testdata/1/1.1/1.1",
|
||||||
|
WalkOption{
|
||||||
|
Recursive: false,
|
||||||
|
},
|
||||||
|
[]string{"中文_ZH (1).txt"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
9,
|
||||||
|
"./testdata/1/1.1/1.1",
|
||||||
|
WalkOption{
|
||||||
|
Recursive: false,
|
||||||
|
},
|
||||||
|
[]string{"中文_ZH (1).txt"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
files := Files(testCase.Path, testCase.Option)
|
||||||
|
for i, file := range files {
|
||||||
|
files[i] = filepath.Base(file)
|
||||||
|
}
|
||||||
|
if !slicex.StringSliceEqual(files, testCase.Files, true, true, true) {
|
||||||
|
t.Errorf("%d: except %v actual %v", testCase.Number, testCase.Files, files)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateDirNames(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
string string
|
||||||
|
n int
|
||||||
|
level int
|
||||||
|
caseSensitive bool
|
||||||
|
dirs []string
|
||||||
|
}{
|
||||||
|
{"t1", "abc", 0, 1, true, []string{"abc"}},
|
||||||
|
{"t2", "abc", 1, 1, true, []string{"a"}},
|
||||||
|
{"t3", "abc", 1, 2, true, []string{"a", "b"}},
|
||||||
|
{"t4", "abc", 1, 3, true, []string{"a", "b", "c"}},
|
||||||
|
{"t5", "abc", 2, 1, true, []string{"ab"}},
|
||||||
|
{"t6", "abc", 2, 2, true, []string{"ab", "c"}},
|
||||||
|
{"t7", " a b c ", 2, 2, true, []string{"ab", "c"}},
|
||||||
|
{"t7", " a b cdefghijklmn ", 2, 3, true, []string{"ab", "cd", "ef"}},
|
||||||
|
{"t8", " a", 12, 3, true, []string{"a"}},
|
||||||
|
{"t9", " a中文$b", 12, 3, true, []string{"ab"}},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
names := GenerateDirNames(test.string, test.n, test.level, test.caseSensitive)
|
||||||
|
assert.Equal(t, test.dirs, names, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExt(t *testing.T) {
|
||||||
|
root, _ := os.Getwd()
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
path string
|
||||||
|
b []byte
|
||||||
|
ext string
|
||||||
|
}{
|
||||||
|
{"t1", "/a/b", nil, ""},
|
||||||
|
{"t2", "https://golang.org/doc/gopher/fiveyears.jpg", nil, ".jpg"},
|
||||||
|
{"t3", filepath.Join(root, "/testdata/2/2.txt"), nil, ".txt"},
|
||||||
|
{"t4", filepath.Join(root, "/testdata/2/1.jpg"), nil, ".jpg"},
|
||||||
|
{"t5", filepath.Join(root, "/testdata/2/1.pdf"), nil, ".pdf"},
|
||||||
|
{"t6", filepath.Join(root, "/testdata/2/1111.pdf"), nil, ".pdf"},
|
||||||
|
{"t7", filepath.Join(root, "/testdata/1.xlsx"), nil, ".xlsx"},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
ext := Ext(test.path, test.b)
|
||||||
|
assert.Equal(t, test.ext, ext, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
0
filepathx/testdata/0.txt
vendored
Normal file
0
filepathx/testdata/0.txt
vendored
Normal file
0
filepathx/testdata/1/1.1/1.1.1/中文_ZH (1).txt
vendored
Normal file
0
filepathx/testdata/1/1.1/1.1.1/中文_ZH (1).txt
vendored
Normal file
0
filepathx/testdata/1/1.1/1.1.1/中文_ZH (9).txt
vendored
Normal file
0
filepathx/testdata/1/1.1/1.1.1/中文_ZH (9).txt
vendored
Normal file
0
filepathx/testdata/1/1.1/1.1.txt
vendored
Normal file
0
filepathx/testdata/1/1.1/1.1.txt
vendored
Normal file
0
filepathx/testdata/1/1.1/1.1/中文_ZH (1).txt
vendored
Normal file
0
filepathx/testdata/1/1.1/1.1/中文_ZH (1).txt
vendored
Normal file
0
filepathx/testdata/2/2.txt
vendored
Normal file
0
filepathx/testdata/2/2.txt
vendored
Normal file
40
filex/file.go
Normal file
40
filex/file.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package filex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsFile Check path is a file
|
||||||
|
func IsFile(path string) bool {
|
||||||
|
fi, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !fi.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir Check path is directory
|
||||||
|
func IsDir(path string) bool {
|
||||||
|
fi, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return fi.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists Check path is exists
|
||||||
|
func Exists(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
if err == nil || os.IsExist(err) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size Return file size
|
||||||
|
func Size(path string) int64 {
|
||||||
|
if fi, err := os.Stat(path); err == nil {
|
||||||
|
return fi.Size()
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
66
filex/file_test.go
Normal file
66
filex/file_test.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package filex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsDir(t *testing.T) {
|
||||||
|
root, _ := os.Getwd()
|
||||||
|
testCases := []struct {
|
||||||
|
Path string
|
||||||
|
Except bool
|
||||||
|
}{
|
||||||
|
{"/a/b", false},
|
||||||
|
{root, true},
|
||||||
|
{root + "/file.go", false},
|
||||||
|
{root + "/file", false},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
v := IsDir(testCase.Path)
|
||||||
|
if v != testCase.Except {
|
||||||
|
t.Errorf("`%s` except %v actual %v", testCase.Path, testCase.Except, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsFile(t *testing.T) {
|
||||||
|
root, _ := os.Getwd()
|
||||||
|
testCases := []struct {
|
||||||
|
Path string
|
||||||
|
Except bool
|
||||||
|
}{
|
||||||
|
{"/a/b", false},
|
||||||
|
{root, false},
|
||||||
|
{root + "/file.go", true},
|
||||||
|
{root + "/file", false},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
v := IsFile(testCase.Path)
|
||||||
|
if v != testCase.Except {
|
||||||
|
t.Errorf("`%s` except %v actual %v", testCase.Path, testCase.Except, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExists(t *testing.T) {
|
||||||
|
root, _ := os.Getwd()
|
||||||
|
testCases := []struct {
|
||||||
|
Path string
|
||||||
|
Except bool
|
||||||
|
}{
|
||||||
|
{"/a/b", false},
|
||||||
|
{root, true},
|
||||||
|
{root + "/file.go", true},
|
||||||
|
{root + "/file", false},
|
||||||
|
{root + "/1.jpg", false},
|
||||||
|
{"https://golang.org/doc/gopher/fiveyears.jpg", false},
|
||||||
|
{"https://golang.org/doc/gopher/not-found.jpg", false},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
v := Exists(testCase.Path)
|
||||||
|
if v != testCase.Except {
|
||||||
|
t.Errorf("`%s` except %v actual %v", testCase.Path, testCase.Except, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
fmtx/fmt.go
Normal file
68
fmtx/fmt.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package fmtx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toJson(prefix string, data interface{}) string {
|
||||||
|
s := ""
|
||||||
|
if b, err := json.MarshalIndent(data, "", " "); err == nil {
|
||||||
|
s = string(b)
|
||||||
|
} else {
|
||||||
|
s = fmt.Sprintf("%#v", data)
|
||||||
|
}
|
||||||
|
if prefix != "" {
|
||||||
|
s = fmt.Sprintf(`%s
|
||||||
|
%s`, prefix, s)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func SprettyPrint(a ...interface{}) string {
|
||||||
|
n := len(a)
|
||||||
|
if n == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
values := make([]string, n)
|
||||||
|
for _, v := range a {
|
||||||
|
values = append(values, toJson("", v))
|
||||||
|
}
|
||||||
|
return strings.Join(values, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrettyPrint(prefix string, a ...interface{}) {
|
||||||
|
onlyOne := len(a) == 1
|
||||||
|
for k, v := range a {
|
||||||
|
p := prefix
|
||||||
|
if p == "" {
|
||||||
|
if !onlyOne {
|
||||||
|
p = strconv.Itoa(k + 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !onlyOne {
|
||||||
|
p = fmt.Sprintf("%s %d", prefix, k+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Print(toJson(prefix, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrettyPrintln(prefix string, a ...interface{}) {
|
||||||
|
onlyOne := len(a) == 1
|
||||||
|
for k, v := range a {
|
||||||
|
p := prefix
|
||||||
|
if p == "" {
|
||||||
|
if !onlyOne {
|
||||||
|
p = strconv.Itoa(k + 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !onlyOne {
|
||||||
|
p = fmt.Sprintf("%s %d", prefix, k+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println(toJson(p, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
10
go.mod
Normal file
10
go.mod
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
module git.cloudyne.io/go/hiscaler-gox
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/stretchr/testify v1.7.0
|
||||||
|
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
|
||||||
|
golang.org/x/text v0.3.7
|
||||||
|
gopkg.in/guregu/null.v4 v4.0.0
|
||||||
|
)
|
||||||
186
htmlx/html.go
Normal file
186
htmlx/html.go
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
package htmlx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"git.cloudyne.io/go/hiscaler-gox/stringx"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Strip regexp
|
||||||
|
rxStrip = regexp.MustCompile(`(?s)<sty(.*)/style>|<scr(.*)/script>|<link(.*)/>|<meta(.*)/>|<!--(.*)-->| style=['"]+(.*)['"]+`)
|
||||||
|
|
||||||
|
// Spaceless regexp
|
||||||
|
rxSpaceless = regexp.MustCompile(`/>\s+</`)
|
||||||
|
|
||||||
|
// Clean regexp
|
||||||
|
rxCleanCSS = regexp.MustCompile(`(?s)<sty(.*)/style>|<link(.*)/>| style=['"]+(.*)['"]+`)
|
||||||
|
rxCleanJavascript = regexp.MustCompile(`(?s)<script(.*)/script>`)
|
||||||
|
rxCleanComment = regexp.MustCompile(`(?s)<!--(.*)-->`)
|
||||||
|
rxCleanMeta = regexp.MustCompile(`(?s)<meta(.*)/>`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type CleanMode uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
CleanModeCSS CleanMode = 1 << (10 - iota) // 包括元素内嵌样式
|
||||||
|
CleanModeJavascript
|
||||||
|
CleanModeComment
|
||||||
|
CleanModeMeta
|
||||||
|
CleanModeSpace
|
||||||
|
CleanModeAll = CleanModeCSS | CleanModeJavascript | CleanModeComment | CleanModeMeta | CleanModeSpace
|
||||||
|
)
|
||||||
|
|
||||||
|
// Strip Clean html tags
|
||||||
|
// https://stackoverflow.com/questions/55036156/how-to-replace-all-html-tag-with-empty-string-in-golang
|
||||||
|
func Strip(html string) string {
|
||||||
|
html = strings.TrimSpace(html)
|
||||||
|
if html != "" {
|
||||||
|
html = rxStrip.ReplaceAllString(html, "")
|
||||||
|
}
|
||||||
|
if html == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
htmlTagStart = 60 // Unicode `<`
|
||||||
|
htmlTagEnd = 62 // Unicode `>`
|
||||||
|
)
|
||||||
|
// Setup a string builder and allocate enough memory for the new string.
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.Grow(len(html) + utf8.UTFMax)
|
||||||
|
|
||||||
|
in := false // True if we are inside an HTML tag.
|
||||||
|
start := 0 // The index of the previous start tag character `<`
|
||||||
|
end := 0 // The index of the previous end tag character `>`
|
||||||
|
|
||||||
|
for i, c := range html {
|
||||||
|
// If this is the last character and we are not in an HTML tag, save it.
|
||||||
|
if (i+1) == len(html) && end >= start && c != htmlTagStart && c != htmlTagEnd {
|
||||||
|
builder.WriteString(html[end:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep going if the character is not `<` or `>`
|
||||||
|
if c != htmlTagStart && c != htmlTagEnd {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == htmlTagStart {
|
||||||
|
// Only update the start if we are not in a tag.
|
||||||
|
// This make sure we strip out `<<br>` not just `<br>`
|
||||||
|
if !in {
|
||||||
|
start = i
|
||||||
|
}
|
||||||
|
in = true
|
||||||
|
|
||||||
|
// Write the valid string between the close and start of the two tags.
|
||||||
|
builder.WriteString(html[end:start])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// else c == htmlTagEnd
|
||||||
|
in = false
|
||||||
|
end = i + 1
|
||||||
|
}
|
||||||
|
s := builder.String()
|
||||||
|
if s != "" {
|
||||||
|
s = strings.TrimSpace(Spaceless(s))
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spaceless 移除多余的空格
|
||||||
|
func Spaceless(html string) string {
|
||||||
|
html = stringx.RemoveExtraSpace(html)
|
||||||
|
if html == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return rxSpaceless.ReplaceAllString(html, "><")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Clean(html string, cleanMode CleanMode) string {
|
||||||
|
if html == "" {
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
const n = 5
|
||||||
|
modes := [n]bool{} // css, javascript, comment, meta, space, all
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if cleanMode&(1<<uint(10-i)) != 0 {
|
||||||
|
modes[i] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if modes[n-1] {
|
||||||
|
html = Spaceless(rxStrip.ReplaceAllString(html, ""))
|
||||||
|
} else {
|
||||||
|
for i := 0; i < n-2; i++ {
|
||||||
|
if modes[i] {
|
||||||
|
switch i {
|
||||||
|
case 0:
|
||||||
|
html = rxCleanCSS.ReplaceAllString(html, "")
|
||||||
|
case 1:
|
||||||
|
html = rxCleanJavascript.ReplaceAllString(html, "")
|
||||||
|
case 2:
|
||||||
|
html = rxCleanComment.ReplaceAllString(html, "")
|
||||||
|
case 3:
|
||||||
|
html = rxCleanMeta.ReplaceAllString(html, "")
|
||||||
|
case 4:
|
||||||
|
html = Spaceless(html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
func Tag(tag, content string, attributes, styles map[string]string) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.Grow(len(tag)*2 + len(content) + 5)
|
||||||
|
sb.WriteString("<")
|
||||||
|
sb.WriteString(tag)
|
||||||
|
fnSortedKeys := func(d map[string]string) []string {
|
||||||
|
n := len(d)
|
||||||
|
if n == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make([]string, n)
|
||||||
|
i := 0
|
||||||
|
for k := range d {
|
||||||
|
keys[i] = k
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if i > 1 {
|
||||||
|
sort.Strings(keys)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, k := range fnSortedKeys(attributes) {
|
||||||
|
sb.WriteString(" ")
|
||||||
|
sb.WriteString(k)
|
||||||
|
sb.WriteString(`="`)
|
||||||
|
sb.WriteString(attributes[k])
|
||||||
|
sb.WriteString(`"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := fnSortedKeys(styles)
|
||||||
|
if len(keys) > 0 {
|
||||||
|
sb.WriteString(` style="`)
|
||||||
|
for _, k := range keys {
|
||||||
|
sb.WriteString(k)
|
||||||
|
sb.WriteString(":")
|
||||||
|
sb.WriteString(styles[k])
|
||||||
|
sb.WriteString(`;`)
|
||||||
|
}
|
||||||
|
sb.WriteString(`"`)
|
||||||
|
}
|
||||||
|
sb.WriteString(">")
|
||||||
|
sb.WriteString(content)
|
||||||
|
sb.WriteString("</")
|
||||||
|
sb.WriteString(tag)
|
||||||
|
sb.WriteString(">")
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
200
htmlx/html_test.go
Normal file
200
htmlx/html_test.go
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
package htmlx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStrip(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
html string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t0", "<div>hello</div>", "hello"},
|
||||||
|
{"t1", `
|
||||||
|
|
||||||
|
<div>hello</div>
|
||||||
|
|
||||||
|
`, "hello"},
|
||||||
|
{"t3", "<div style='font-size: 12px;'>hello</div>", "hello"},
|
||||||
|
{"t4", "<style>body {font-size: 12px}</style><div style='font-size: 12px;'>hello</div>", "hello"},
|
||||||
|
{"t4", `
|
||||||
|
<link rel='stylesheet' id='wp-block-library-css' href='https://www.example.com/style.min.css?ver=5.9.1' type='text/css' media='all' />
|
||||||
|
<style type="text/css">body {font-size: 12px}</style><!-- / See later. --><div style='font-size: 12px;'>hello</div>`, "hello"},
|
||||||
|
{"t5", `
|
||||||
|
<body class="nodata company_blog" style="">
|
||||||
|
<script> var toolbarSearchExt = '{"landingWord":[],"queryWord":"","tag":["function","class","filter","search"],"title":"Yii: 设置数据翻页"}';
|
||||||
|
</script>
|
||||||
|
<script src="https://g.csdnimg.cn/common/csdn-toolbar/csdn-toolbar.js" type="text/javascript"></script>
|
||||||
|
<script src="https://g.csdnimg.cn/common/csdn-toolbar/csdn-toolbar1.js" type="text/javascript"></script>
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var bp = document.createElement('script');
|
||||||
|
var curProtocol = window.location.protocol.split(':')[0];
|
||||||
|
if (curProtocol === 'https') {
|
||||||
|
bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
bp.src = 'http://push.zhanzhang.baidu.com/push.js';
|
||||||
|
}
|
||||||
|
var s = document.getElementsByTagName("script")[0];
|
||||||
|
s.parentNode.insertBefore(bp, s);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="https://csdnimg.cn/release/blogv2/dist/pc/css/blog_code-01256533b5.min.css">
|
||||||
|
<link rel="stylesheet" href="https://csdnimg.cn/release/blogv2/dist/mdeditor/css/editerView/chart-3456820cac.css" /><div style='font-size: 12px;'>hello</div></body>`, "hello"},
|
||||||
|
{"t6", `<!-- show up to 2 reviews by default -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<p>Custom flags for your garden are a great way to show your personality to your friends and neighbors. Design and turn it into an eye-catching flag all year round. This will be a beautiful addition to your yard and garden, also a simple sign to show your patriotism on Memorial Day, 4th of July or Veterans Day, Christmas holidays or any holiday of the year.
|
||||||
|
|
||||||
|
</p>`, "Custom flags for your garden are a great way to show your personality to your friends and neighbors. Design and turn it into an eye-catching flag all year round. This will be a beautiful addition to your yard and garden, also a simple sign to show your patriotism on Memorial Day, 4th of July or Veterans Day, Christmas holidays or any holiday of the year."},
|
||||||
|
{"t7", "<div>hello<div>", "hello"},
|
||||||
|
{"t8", " <div> hello world <div>", "hello world"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
equal := Strip(test.html)
|
||||||
|
assert.Equal(t, test.expected, equal, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkStrip(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
Strip(`<!-- show up to 2 reviews by default -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<p>Custom flags for your garden are a great way to show your personality to your friends and neighbors. Design and turn it into an eye-catching flag all year round. This will be a beautiful addition to your yard and garden, also a simple sign to show your patriotism on Memorial Day, 4th of July or Veterans Day, Christmas holidays or any holiday of the year.
|
||||||
|
|
||||||
|
</p>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSpaceless(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
html string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t0", "<div>hello</div>", "<div>hello</div>"},
|
||||||
|
{"t1", `
|
||||||
|
|
||||||
|
<div>hello</div>
|
||||||
|
|
||||||
|
`, "<div>hello</div>"},
|
||||||
|
{"t3", "<div style='font-size: 12px;'>hello</div>", "<div style='font-size: 12px;'>hello</div>"},
|
||||||
|
{"t4", "<style>body {font-size: 12px}</style><div style='font-size: 12px;'>hello</div>", "<style>body {font-size: 12px}</style><div style='font-size: 12px;'>hello</div>"},
|
||||||
|
{"t4", `
|
||||||
|
<link rel='stylesheet' id='wp-block-library-css' href='https://www.example.com/style.min.css?ver=5.9.1' type='text/css' media='all' />
|
||||||
|
<style type="text/css">body {font-size: 12px}</style><!-- / See later. --><div style='font-size: 12px;'>hello</div>`, `<link rel='stylesheet' id='wp-block-library-css' href='https://www.example.com/style.min.css?ver=5.9.1' type='text/css' media='all' />
|
||||||
|
<style type="text/css">body {font-size: 12px}</style><!-- / See later. --><div style='font-size: 12px;'>hello</div>`},
|
||||||
|
{"t7", "<div> hello </div> <span></span>", "<div> hello </div> <span></span>"},
|
||||||
|
{"t8", `<!-- show up to 2 reviews by default -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<p>Custom flags for your garden are a great way to show your personality to your friends and neighbors. Design and turn it into an eye-catching flag all year round. This will be a beautiful addition to your yard and garden, also a simple sign to show your patriotism on Memorial Day, 4th of July or Veterans Day, Christmas holidays or any holiday of the year.
|
||||||
|
|
||||||
|
</p>`, `<!-- show up to 2 reviews by default --> <p>Custom flags for your garden are a great way to show your personality to your friends and neighbors. Design and turn it into an eye-catching flag all year round. This will be a beautiful addition to your yard and garden, also a simple sign to show your patriotism on Memorial Day, 4th of July or Veterans Day, Christmas holidays or any holiday of the year. </p>`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
html := Spaceless(test.html)
|
||||||
|
assert.Equal(t, test.expected, html, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClean(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
html string
|
||||||
|
cleanMode CleanMode
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"tcss1", "<div>hello</div>", CleanModeCSS, "<div>hello</div>"},
|
||||||
|
{"tcss2", "<style>body {font-size: 12px}</style><div style='font-size: 12px;'>hello</div>", CleanModeCSS, "<div>hello</div>"},
|
||||||
|
{"tjavascript1", `<script src="//www.a.com/1.8.5/blog.js" type='text/javascript'></script><style>body {font-size: 12px}</style><div style='font-size: 12px;'>hello</div>`, CleanModeJavascript, "<style>body {font-size: 12px}</style><div style='font-size: 12px;'>hello</div>"},
|
||||||
|
{"tcomment1", `<script src="//www.a.com/1.8.5/blog.js" type='text/javascript'></script><!--comment--><style>body {font-size: 12px}</style><div style='font-size: 12px;'>hello</div>`, CleanModeComment, "<script src=\"//www.a.com/1.8.5/blog.js\" type='text/javascript'></script><style>body {font-size: 12px}</style><div style='font-size: 12px;'>hello</div>"},
|
||||||
|
{"tcss,javascript,comment", `<script src="//www.a.com/1.8.5/blog.js" type='text/javascript'></script><!--comment--><style>body {font-size: 12px}</style><div style='font-size: 12px;'>hello</div>`, CleanModeCSS | CleanModeJavascript | CleanModeComment, "<div>hello</div>"},
|
||||||
|
{"tall1", `<script>alert("ddd")</script><style>body {font-size: 12px}</style><div style='font-size: 12px;'>hello</div>`, CleanModeAll, "<div>hello</div>"},
|
||||||
|
{"tall2", `<!-- show up to 2 reviews by default -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<p>Product details: +++ Material: 100% Ceramic +++ Size: 11oz or 15oz +++ Dye Sublimation graphics for exceptional prints. +++ Dishwasher and microwave safe. +++ Image is printed on both sides of mug. +++ Printed in the U.S.A. +++ Shipping info: Shipping time is approximately 5-7 business days.
|
||||||
|
|
||||||
|
</p>`, CleanModeAll, "<p>Product details: +++ Material: 100% Ceramic +++ Size: 11oz or 15oz +++ Dye Sublimation graphics for exceptional prints. +++ Dishwasher and microwave safe. +++ Image is printed on both sides of mug. +++ Printed in the U.S.A. +++ Shipping info: Shipping time is approximately 5-7 business days. </p>"},
|
||||||
|
{"tall3", `<div> 1 2 </div> <div>2</div>`, CleanModeAll, `<div> 1 2 </div> <div>2</div>`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range tests {
|
||||||
|
html := Clean(testCase.html, testCase.cleanMode)
|
||||||
|
assert.Equal(t, testCase.expected, html, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTag(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
elementTag string
|
||||||
|
content string
|
||||||
|
attributes map[string]string
|
||||||
|
styles map[string]string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t0", "div", "hello", nil, nil, "<div>hello</div>"},
|
||||||
|
{"t1", "div", "hello", map[string]string{"id": "name"}, nil, `<div id="name">hello</div>`},
|
||||||
|
{"t1.1", "div", "hello", map[string]string{"id": "name", "name": "name"}, nil, `<div id="name" name="name">hello</div>`},
|
||||||
|
{"t2", "div", "hello", map[string]string{"id": "name", "data-tag": "123"}, map[string]string{"font-size": "1", "font-weight": "bold"}, `<div data-tag="123" id="name" style="font-size:1;font-weight:bold;">hello</div>`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
equal := Tag(test.elementTag, test.content, test.attributes, test.styles)
|
||||||
|
assert.Equal(t, test.expected, equal, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkTag(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
Tag("div", "hello", map[string]string{"id": "name"}, map[string]string{"font-size": "1"})
|
||||||
|
}
|
||||||
|
}
|
||||||
37
inx/in.go
Normal file
37
inx/in.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package inx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// In Check value in values, return true if in values, otherwise return false.
|
||||||
|
// Value T is a generic value
|
||||||
|
func In[T comparable](value T, values []T) bool {
|
||||||
|
if values == nil || len(values) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, v := range values {
|
||||||
|
if v == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringIn 判断 s 是否在 ss 中(忽略大小写)
|
||||||
|
func StringIn(s string, ss ...string) bool {
|
||||||
|
if len(ss) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, s2 := range ss {
|
||||||
|
if strings.EqualFold(s, s2) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntIn 判断 i 是否在 ii 中
|
||||||
|
func IntIn(i int, ii ...int) bool {
|
||||||
|
return In(i, ii)
|
||||||
|
}
|
||||||
67
inx/in_test.go
Normal file
67
inx/in_test.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package inx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIn(t *testing.T) {
|
||||||
|
assert.Equal(t, true, In(1, []int{1, 2, 3, 4}), "int1")
|
||||||
|
assert.Equal(t, false, In(1, []int{2, 3, 4, 5}), "int2")
|
||||||
|
assert.Equal(t, false, In(1, nil), "int3")
|
||||||
|
assert.Equal(t, false, In(1, []int{}), "int4")
|
||||||
|
assert.Equal(t, true, In(1, []float64{1.0, 2.0, 3.0}), "float1")
|
||||||
|
assert.Equal(t, false, In(1.1, []float64{1.0, 2.0, 3.0}), "float2")
|
||||||
|
assert.Equal(t, true, In(true, []bool{true, false, false}), "bool1")
|
||||||
|
assert.Equal(t, false, In(true, []bool{false, false, false}), "bool2")
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkIn(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
ss := make([]string, 100000)
|
||||||
|
for i := 0; i < 100000; i++ {
|
||||||
|
ss[i] = strconv.Itoa(100000 - i)
|
||||||
|
}
|
||||||
|
sort.Strings(ss)
|
||||||
|
b.StartTimer()
|
||||||
|
In("1", ss)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkStringIn(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
ss := make([]string, 100000)
|
||||||
|
for i := 0; i < 100000; i++ {
|
||||||
|
ss[i] = strconv.Itoa(100000 - i)
|
||||||
|
}
|
||||||
|
b.StartTimer()
|
||||||
|
StringIn("1", ss...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntIn(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
i int
|
||||||
|
ii []int
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"t1", 1, []int{1, 2, 3, 4, 4}, true},
|
||||||
|
{"t2", 1, []int{2, 3, 4, 5}, false},
|
||||||
|
{"t3", 1, nil, false},
|
||||||
|
{"t4", 1, []int{}, false},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
assert.Equal(t, testCase.expected, IntIn(testCase.i, testCase.ii...), testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkIntIn(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
ii := make([]int, 100000)
|
||||||
|
for i := 0; i < 100000; i++ {
|
||||||
|
ii[i] = 100000 - i
|
||||||
|
}
|
||||||
|
b.StartTimer()
|
||||||
|
IntIn(1, ii...)
|
||||||
|
}
|
||||||
123
ipx/ip.go
Normal file
123
ipx/ip.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package ipx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RemoteAddr(r *http.Request, mustPublic bool) string {
|
||||||
|
if r == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range []string{"X-Forwarded-For", "X-Real-IP", "X-Appengine-Remote-Addr"} {
|
||||||
|
value := r.Header.Get(key)
|
||||||
|
if value != "" {
|
||||||
|
for _, item := range strings.Split(value, ",") {
|
||||||
|
var ip string
|
||||||
|
if strings.ContainsRune(item, ':') {
|
||||||
|
if host, _, err := net.SplitHostPort(strings.TrimSpace(item)); err != nil {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
ip = host
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ip = strings.TrimSpace(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mustPublic {
|
||||||
|
if v, e := IsPublic(ip); e != nil && v {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.RemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func LocalAddr() string {
|
||||||
|
addresses, err := net.InterfaceAddrs()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := ""
|
||||||
|
for _, address := range addresses {
|
||||||
|
if ipNet, ok := address.(*net.IPNet); ok &&
|
||||||
|
!ipNet.IP.IsLoopback() &&
|
||||||
|
!ipNet.IP.IsPrivate() &&
|
||||||
|
!ipNet.IP.IsLinkLocalUnicast() {
|
||||||
|
if ipNet.IP.To4() != nil {
|
||||||
|
addr = ipNet.IP.String()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if addr == "" {
|
||||||
|
for _, address := range []string{"114.114.114.114:53", "8.8.8.8:53"} {
|
||||||
|
var conn net.Conn
|
||||||
|
conn, err = net.Dial("udp", address)
|
||||||
|
if err == nil {
|
||||||
|
conn.Close()
|
||||||
|
localAddr := conn.LocalAddr().(*net.UDPAddr)
|
||||||
|
addr = strings.Split(localAddr.String(), ":")[0]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsPrivate(ip string) (v bool, err error) {
|
||||||
|
addr := net.ParseIP(ip)
|
||||||
|
if addr == nil {
|
||||||
|
err = fmt.Errorf("ipx: %s address is invalid", ip)
|
||||||
|
} else {
|
||||||
|
v = addr.IsLoopback() || addr.IsPrivate() || addr.IsLinkLocalUnicast()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsPublic(ip string) (v bool, err error) {
|
||||||
|
v, err = IsPrivate(ip)
|
||||||
|
v = err == nil && !v
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Number(ip string) (uint, error) {
|
||||||
|
addr := net.ParseIP(ip)
|
||||||
|
if addr == nil {
|
||||||
|
return 0, fmt.Errorf("ipx: %s is invalid ip", ip)
|
||||||
|
}
|
||||||
|
return uint(addr[3]) | uint(addr[2])<<8 | uint(addr[1])<<16 | uint(addr[0])<<24, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Random() string {
|
||||||
|
size := 4
|
||||||
|
ip := make([]byte, size)
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
ip[i] = byte(rand.Intn(256))
|
||||||
|
}
|
||||||
|
return net.IP(ip).To4().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func String(ip uint) (string, error) {
|
||||||
|
if ip > math.MaxUint32 {
|
||||||
|
return "", fmt.Errorf("ipx: %d is not valid ipv4", ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := make(net.IP, net.IPv4len)
|
||||||
|
addr[0] = byte(ip >> 24)
|
||||||
|
addr[1] = byte(ip >> 16)
|
||||||
|
addr[2] = byte(ip >> 8)
|
||||||
|
addr[3] = byte(ip)
|
||||||
|
return addr.String(), nil
|
||||||
|
}
|
||||||
99
ipx/ip_test.go
Normal file
99
ipx/ip_test.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package ipx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRemoteAddr(t *testing.T) {
|
||||||
|
request := &http.Request{
|
||||||
|
Header: map[string][]string{},
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
headers map[string][]string
|
||||||
|
mustPublic bool
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"t1", map[string][]string{
|
||||||
|
"X-Real-IP": {"127.0.0.1"},
|
||||||
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
|
}, false, "127.0.0.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t2", map[string][]string{
|
||||||
|
"X-Real-IP": {"127.0.0.1:8080"},
|
||||||
|
"X-Forwarded-For": {"127.0.0.1:8080"},
|
||||||
|
}, false, "127.0.0.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t3", map[string][]string{
|
||||||
|
"X-Real-IP": {"127.0.0.1"},
|
||||||
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
|
}, true, "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t4", map[string][]string{
|
||||||
|
"X-Real-IP": {"127.0.0.1:8080"},
|
||||||
|
"X-Forwarded-For": {"127.0.0.1:8080"},
|
||||||
|
}, true, "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t5", map[string][]string{
|
||||||
|
"X-Real-IP": {"::1"},
|
||||||
|
"X-Forwarded-For": {"::1"},
|
||||||
|
}, true, "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
request.Header = testCase.headers
|
||||||
|
addr := RemoteAddr(request, testCase.mustPublic)
|
||||||
|
assert.Equal(t, testCase.expected, addr, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocalAddr(t *testing.T) {
|
||||||
|
ip := LocalAddr()
|
||||||
|
if ip == "" {
|
||||||
|
t.Error("LocalAddr() return empty value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsPrivate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
ip string
|
||||||
|
expected bool
|
||||||
|
hasError bool
|
||||||
|
}{
|
||||||
|
{"t1", "127.0.0.1", true, false},
|
||||||
|
{"t2", "::1", true, false},
|
||||||
|
{"t3", "xxx", false, true},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
v, err := IsPrivate(testCase.ip)
|
||||||
|
assert.Equal(t, testCase.expected, v, testCase.tag)
|
||||||
|
assert.Equal(t, testCase.hasError, err != nil, testCase.tag+" error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsPublic(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
ip string
|
||||||
|
expected bool
|
||||||
|
hasError bool
|
||||||
|
}{
|
||||||
|
{"t1", "127.0.0.1", false, false},
|
||||||
|
{"t2", "::1", false, false},
|
||||||
|
{"t3", "xxx", false, true},
|
||||||
|
{"t4", "120.228.142.126", true, false},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
v, err := IsPublic(testCase.ip)
|
||||||
|
assert.Equal(t, testCase.expected, v, testCase.tag)
|
||||||
|
assert.Equal(t, testCase.hasError, err != nil, testCase.tag+" error")
|
||||||
|
}
|
||||||
|
}
|
||||||
177
isx/is.go
Normal file
177
isx/is.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package isx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"git.cloudyne.io/go/hiscaler-gox/stringx"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
rxSafeCharacters = regexp.MustCompile("^[a-zA-Z0-9\\.\\-_][a-zA-Z0-9\\.\\-_]*$")
|
||||||
|
rxNumber = regexp.MustCompile("^[+-]?\\d+$|^\\d+[.]\\d+$")
|
||||||
|
rxColorHex = regexp.MustCompile("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")
|
||||||
|
)
|
||||||
|
|
||||||
|
// OS type
|
||||||
|
const (
|
||||||
|
IsAix = "aix"
|
||||||
|
IsAndroid = "android"
|
||||||
|
IsDarwin = "darwin"
|
||||||
|
IsDragonfly = "dragonfly"
|
||||||
|
IsFreebsd = "freebsd"
|
||||||
|
IsHurd = "hurd"
|
||||||
|
IsIllumos = "illumos"
|
||||||
|
IsIos = "ios"
|
||||||
|
IsJs = "js"
|
||||||
|
IsLinux = "linux"
|
||||||
|
IsNacl = "nacl"
|
||||||
|
IsNetbsd = "netbsd"
|
||||||
|
IsOpenbsd = "openbsd"
|
||||||
|
IsPlan9 = "plan9"
|
||||||
|
IsSolaris = "solaris"
|
||||||
|
IsWindows = "windows"
|
||||||
|
IsZos = "zos"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Number Check any value is a number
|
||||||
|
func Number(i interface{}) bool {
|
||||||
|
switch i.(type) {
|
||||||
|
case string:
|
||||||
|
s := stringx.TrimAny(strings.TrimSpace(i.(string)), "+", "-")
|
||||||
|
n := len(s)
|
||||||
|
if n == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.IndexFunc(s[n-1:], func(c rune) bool {
|
||||||
|
return c < '0' || c > '9'
|
||||||
|
}) != -1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return rxNumber.MatchString(strings.ReplaceAll(s, ",", ""))
|
||||||
|
case int, int8, int16, int32, int64,
|
||||||
|
uint, uint8, uint16, uint32, uint64, uintptr,
|
||||||
|
float32, float64,
|
||||||
|
complex64, complex128:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty 判断是否为空
|
||||||
|
func Empty(value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
v := reflect.ValueOf(value)
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.String, reflect.Array, reflect.Map, reflect.Slice:
|
||||||
|
return v.Len() == 0
|
||||||
|
case reflect.Bool:
|
||||||
|
return !v.Bool()
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return v.Int() == 0
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
return v.Uint() == 0
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return v.Float() == 0
|
||||||
|
case reflect.Invalid:
|
||||||
|
return true
|
||||||
|
case reflect.Interface, reflect.Ptr:
|
||||||
|
if v.IsNil() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return Empty(v.Elem().Interface())
|
||||||
|
case reflect.Struct:
|
||||||
|
v, ok := value.(time.Time)
|
||||||
|
if ok && v.IsZero() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func Equal(expected interface{}, actual interface{}) bool {
|
||||||
|
if expected == nil || actual == nil {
|
||||||
|
return expected == actual
|
||||||
|
}
|
||||||
|
|
||||||
|
if exp, ok := expected.([]byte); ok {
|
||||||
|
act, ok := actual.([]byte)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if exp == nil || act == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.Equal(exp, act)
|
||||||
|
}
|
||||||
|
return reflect.DeepEqual(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SafeCharacters Only include a-zA-Z0-9.-_
|
||||||
|
// Reference https://www.quora.com/What-are-valid-file-names
|
||||||
|
func SafeCharacters(str string) bool {
|
||||||
|
if str == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return rxSafeCharacters.MatchString(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HttpURL checks if the string is a HTTP URL.
|
||||||
|
// govalidator/IsURL
|
||||||
|
func HttpURL(str string) bool {
|
||||||
|
const (
|
||||||
|
URLSchema string = `((https?):\/\/)`
|
||||||
|
URLPath string = `((\/|\?|#)[^\s]*)`
|
||||||
|
URLPort string = `(:(\d{1,5}))`
|
||||||
|
URLIP string = `([1-9]\d?|1\d\d|2[01]\d|22[0-3]|24\d|25[0-5])(\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-5]))`
|
||||||
|
URLSubdomain string = `((www\.)|([a-zA-Z0-9]+([-_\.]?[a-zA-Z0-9])*[a-zA-Z0-9]\.[a-zA-Z0-9]+))`
|
||||||
|
URL = `^` + URLSchema + `?` + `((` + URLIP + `|(\[` + `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))` + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-_]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + URLSubdomain + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))\.?` + URLPort + `?` + URLPath + `?$`
|
||||||
|
)
|
||||||
|
|
||||||
|
if str == "" || utf8.RuneCountInString(str) >= 2083 || len(str) <= 3 || strings.HasPrefix(str, ".") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(str, "//") {
|
||||||
|
str = "http:" + str
|
||||||
|
}
|
||||||
|
strTemp := str
|
||||||
|
if strings.Contains(str, ":") && !strings.Contains(str, "://") {
|
||||||
|
// support no indicated urlscheme but with colon for port number
|
||||||
|
// http:// is appended so url.Parse will succeed, strTemp used so it does not impact rxURL.MatchString
|
||||||
|
strTemp = "http://" + str
|
||||||
|
}
|
||||||
|
u, err := url.Parse(strTemp)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(u.Host, ".") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if u.Host == "" && (u.Path != "" && !strings.Contains(u.Path, ".")) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return regexp.MustCompile(URL).MatchString(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OS check typ is a valid OS type
|
||||||
|
// Usage: isx.OS(isx.IsLinux)
|
||||||
|
func OS(typ string) bool {
|
||||||
|
return runtime.GOOS == typ
|
||||||
|
}
|
||||||
|
|
||||||
|
func ColorHex(s string) bool {
|
||||||
|
return rxColorHex.MatchString(s)
|
||||||
|
}
|
||||||
231
isx/is_test.go
Normal file
231
isx/is_test.go
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
package isx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNumber(t *testing.T) {
|
||||||
|
uintPtr := uintptr(12)
|
||||||
|
testCases := []struct {
|
||||||
|
Value interface{}
|
||||||
|
IsNumber bool
|
||||||
|
}{
|
||||||
|
{"a", false},
|
||||||
|
{"111", true},
|
||||||
|
{"1.23", true},
|
||||||
|
{"1,234.5", true},
|
||||||
|
{"1234.5,", false},
|
||||||
|
{"12345.", false},
|
||||||
|
{" 12345.6 ", true},
|
||||||
|
{" 12345. 6 ", false},
|
||||||
|
{"-1", true},
|
||||||
|
{"+1", true},
|
||||||
|
{1, true},
|
||||||
|
{1.1, true},
|
||||||
|
{0, true},
|
||||||
|
{uintPtr, true},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
v := Number(testCase.Value)
|
||||||
|
if v != testCase.IsNumber {
|
||||||
|
t.Errorf("%s except %v actual %v", testCase.Value, testCase.IsNumber, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmpty(t *testing.T) {
|
||||||
|
var s1 string
|
||||||
|
var s2 = "a"
|
||||||
|
var s3 *string
|
||||||
|
s4 := struct{}{}
|
||||||
|
time1 := time.Now()
|
||||||
|
var time2 time.Time
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
value interface{}
|
||||||
|
empty bool
|
||||||
|
}{
|
||||||
|
// nil
|
||||||
|
{"t0", nil, true},
|
||||||
|
// string
|
||||||
|
{"t1.1", "", true},
|
||||||
|
{"t1.2", "1", false},
|
||||||
|
// slice
|
||||||
|
{"t2.1", []byte(""), true},
|
||||||
|
{"t2.2", []byte("1"), false},
|
||||||
|
// map
|
||||||
|
{"t3.1", map[string]int{}, true},
|
||||||
|
{"t3.2", map[string]int{"a": 1}, false},
|
||||||
|
// bool
|
||||||
|
{"t4.1", false, true},
|
||||||
|
{"t4.2", true, false},
|
||||||
|
// int
|
||||||
|
{"t5.1", 0, true},
|
||||||
|
{"t5.2", int8(0), true},
|
||||||
|
{"t5.3", int16(0), true},
|
||||||
|
{"t5.4", int32(0), true},
|
||||||
|
{"t5.5", int64(0), true},
|
||||||
|
{"t5.6", 1, false},
|
||||||
|
{"t5.7", int8(1), false},
|
||||||
|
{"t5.8", int16(1), false},
|
||||||
|
{"t5.9", int32(1), false},
|
||||||
|
{"t5.10", int64(1), false},
|
||||||
|
// uint
|
||||||
|
{"t6.1", uint(0), true},
|
||||||
|
{"t6.2", uint8(0), true},
|
||||||
|
{"t6.3", uint16(0), true},
|
||||||
|
{"t6.4", uint32(0), true},
|
||||||
|
{"t6.5", uint64(0), true},
|
||||||
|
{"t6.6", uint(1), false},
|
||||||
|
{"t6.7", uint8(1), false},
|
||||||
|
{"t6.8", uint16(1), false},
|
||||||
|
{"t6.9", uint32(1), false},
|
||||||
|
{"t6.10", uint64(1), false},
|
||||||
|
// float
|
||||||
|
{"t7.1", float32(0), true},
|
||||||
|
{"t7.2", float64(0), true},
|
||||||
|
{"t7.3", float32(1), false},
|
||||||
|
{"t7.4", float64(1), false},
|
||||||
|
// interface, ptr
|
||||||
|
{"t8.1", &s1, true},
|
||||||
|
{"t8.2", &s2, false},
|
||||||
|
{"t8.3", s3, true},
|
||||||
|
// struct
|
||||||
|
{"t9.1", s4, false},
|
||||||
|
{"t9.2", &s4, false},
|
||||||
|
// time.Time
|
||||||
|
{"t10.1", time1, false},
|
||||||
|
{"t10.2", &time1, false},
|
||||||
|
{"t10.3", time2, true},
|
||||||
|
{"t10.4", &time2, true},
|
||||||
|
// rune
|
||||||
|
{"t11.1", 'a', false},
|
||||||
|
// byte
|
||||||
|
{"t12.1", []byte(""), true},
|
||||||
|
{"t12.2", []byte(" "), false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
empty := Empty(test.value)
|
||||||
|
assert.Equal(t, test.empty, empty, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsEqual(t *testing.T) {
|
||||||
|
s1 := "hello"
|
||||||
|
s2 := s1
|
||||||
|
s3 := "hello"
|
||||||
|
t1 := time.Now()
|
||||||
|
t2 := time.Now().AddDate(0, 0, 1)
|
||||||
|
type1 := []struct {
|
||||||
|
username string
|
||||||
|
}{
|
||||||
|
{"john"},
|
||||||
|
}
|
||||||
|
type2 := []struct {
|
||||||
|
username string
|
||||||
|
}{
|
||||||
|
{"john"},
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
a interface{}
|
||||||
|
b interface{}
|
||||||
|
except bool
|
||||||
|
}{
|
||||||
|
{"t0", nil, nil, true},
|
||||||
|
{"t1", nil, "", false},
|
||||||
|
{"t2", "", "", true},
|
||||||
|
{"t3", "", " ", false},
|
||||||
|
{"t4", s1, s2, true},
|
||||||
|
{"t5", s2, s3, true},
|
||||||
|
{"t6", t1, t2, false},
|
||||||
|
{"t7", type1, type2, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
equal := Equal(test.a, test.b)
|
||||||
|
assert.Equal(t, test.except, equal, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSafeCharacters(t *testing.T) {
|
||||||
|
type testCast struct {
|
||||||
|
String string
|
||||||
|
Safe bool
|
||||||
|
}
|
||||||
|
testCasts := []testCast{
|
||||||
|
{"", false},
|
||||||
|
{" ", false},
|
||||||
|
{"a", true},
|
||||||
|
{"111", true},
|
||||||
|
{"a", false},
|
||||||
|
{"A_B", true},
|
||||||
|
{"A_中B", false},
|
||||||
|
{"a.b-c_", true},
|
||||||
|
{"_.a.b-c_", true},
|
||||||
|
{`\.a.b-c_`, false},
|
||||||
|
}
|
||||||
|
for _, tc := range testCasts {
|
||||||
|
safe := SafeCharacters(tc.String)
|
||||||
|
if safe != tc.Safe {
|
||||||
|
t.Errorf("%s except %v, actual:%v", tc.String, tc.Safe, safe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSafeCharacters(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
SafeCharacters("_.a.b-c_")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHttpURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
url string
|
||||||
|
except bool
|
||||||
|
}{
|
||||||
|
{"t0", "www.example.com", true},
|
||||||
|
{"t1", "http://www.example.com", true},
|
||||||
|
{"t2", "https://www.example.com", true},
|
||||||
|
{"t3", "https://www.com", true},
|
||||||
|
{"t4", "https://a", true}, // is valid URL?
|
||||||
|
{"t5", "https://127.0.0.1", true},
|
||||||
|
{"t6", "https://", false},
|
||||||
|
{"t7", "https://a", true},
|
||||||
|
{"t8", "", false},
|
||||||
|
{"t9", "aaa", false},
|
||||||
|
{"t10", "https://www.example.com:8080", true},
|
||||||
|
{"t11", "//www.example.com:8080", true},
|
||||||
|
{"t12", "//a.b", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
equal := HttpURL(test.url)
|
||||||
|
assert.Equal(t, test.except, equal, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColorHex(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
color string
|
||||||
|
except bool
|
||||||
|
}{
|
||||||
|
{"t0", "#fff", true},
|
||||||
|
{"t1", "#ffffff", true},
|
||||||
|
{"t2", "#000000", true},
|
||||||
|
{"t3", "#ffff", false},
|
||||||
|
{"t4", "ffffff", false},
|
||||||
|
{"t5", "#ggg", false},
|
||||||
|
{"t6", "#-100000", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
equal := ColorHex(test.color)
|
||||||
|
assert.Equal(t, test.except, equal, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
157
jsonx/json.go
Normal file
157
jsonx/json.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package jsonx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ToRawMessage(i interface{}, defaultValue string) (json.RawMessage, error) {
|
||||||
|
m := json.RawMessage{}
|
||||||
|
var b []byte
|
||||||
|
var err error
|
||||||
|
b, err = json.Marshal(&i)
|
||||||
|
if err != nil {
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b = bytes.TrimSpace(b)
|
||||||
|
if len(b) == 0 || bytes.EqualFold(b, []byte("null")) {
|
||||||
|
b = []byte(defaultValue)
|
||||||
|
}
|
||||||
|
err = m.UnmarshalJSON(b)
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJson Change interface to json string
|
||||||
|
func ToJson(i interface{}, defaultValue string) string {
|
||||||
|
if i == nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
vo := reflect.ValueOf(i)
|
||||||
|
switch vo.Kind() {
|
||||||
|
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice:
|
||||||
|
if vo.IsNil() {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = json.Compact(&buf, b)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
if json.Valid(buf.Bytes()) {
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPrettyJson(i interface{}) string {
|
||||||
|
if i == nil {
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
vo := reflect.ValueOf(i)
|
||||||
|
switch vo.Kind() {
|
||||||
|
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice:
|
||||||
|
if vo.IsNil() {
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("%+v", i)
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = json.Indent(&buf, b, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("%+v", i)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmptyObjectRawMessage 空对象
|
||||||
|
func EmptyObjectRawMessage() json.RawMessage {
|
||||||
|
v := json.RawMessage{}
|
||||||
|
_ = v.UnmarshalJSON([]byte("{}"))
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmptyArrayRawMessage 空数组
|
||||||
|
func EmptyArrayRawMessage() json.RawMessage {
|
||||||
|
v := json.RawMessage{}
|
||||||
|
_ = v.UnmarshalJSON([]byte("[]"))
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmptyRawMessage 验证数据是否为空
|
||||||
|
func IsEmptyRawMessage(data json.RawMessage) bool {
|
||||||
|
if data == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := data.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
s := string(bytes.TrimSpace(b))
|
||||||
|
if s == "" || s == "[]" || s == "{}" || strings.EqualFold(s, "null") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Index(s, " ") != -1 {
|
||||||
|
s = strings.ReplaceAll(s, " ", "")
|
||||||
|
}
|
||||||
|
return s == "[]" || s == "{}"
|
||||||
|
}
|
||||||
|
|
||||||
|
func Convert(from json.RawMessage, to any) error {
|
||||||
|
if IsEmptyRawMessage(from) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var b []byte
|
||||||
|
b, err := from.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(b, &to)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract 提取字符串中的有效 JSON 数据
|
||||||
|
// 比如 `{"a": 1, "b": 2}}}}a` 提取后的数据为 `{"a": 1, "b": 2}`
|
||||||
|
func Extract(str string) (string, error) {
|
||||||
|
str = strings.TrimSpace(str)
|
||||||
|
n := len(str)
|
||||||
|
if n == 0 {
|
||||||
|
return "", errors.New("jsonx: empty string")
|
||||||
|
}
|
||||||
|
if json.Valid([]byte(str)) {
|
||||||
|
return str, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if str[i] == '{' || str[i] == '[' {
|
||||||
|
for j := n; j > i; j-- {
|
||||||
|
substr := str[i:j]
|
||||||
|
if json.Valid([]byte(substr)) {
|
||||||
|
return substr, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("jsonx: not found")
|
||||||
|
}
|
||||||
166
jsonx/json_test.go
Normal file
166
jsonx/json_test.go
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package jsonx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestToJson(t *testing.T) {
|
||||||
|
var names []string
|
||||||
|
testCases := []struct {
|
||||||
|
Number int
|
||||||
|
Value interface{}
|
||||||
|
DefaultValue string
|
||||||
|
Except string
|
||||||
|
}{
|
||||||
|
{1, []string{}, "[]", "[]"},
|
||||||
|
{2, struct{}{}, "", "{}"},
|
||||||
|
{3, struct {
|
||||||
|
Name string
|
||||||
|
Age int
|
||||||
|
}{"Hello", 12}, "", `{"Name":"hello","Age":12}`},
|
||||||
|
{4, struct {
|
||||||
|
Name string `json:"a"`
|
||||||
|
Age int `json:"b"`
|
||||||
|
}{"Hello", 12}, "", `{"a":"hello","b":12}`},
|
||||||
|
{5, nil, "abc", "abc"},
|
||||||
|
{6, []int{1, 2}, "null", "[1,2]"},
|
||||||
|
{7, []string{"a", "b"}, "null", `["a","b"]`},
|
||||||
|
{8, 1, "[]", "1"},
|
||||||
|
{9, "abc", "[]", `"abc"`},
|
||||||
|
{10, nil, "[]", `[]`},
|
||||||
|
{11, names, "[]", `[]`},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
s := ToJson(testCase.Value, testCase.DefaultValue)
|
||||||
|
if !strings.EqualFold(s, testCase.Except) {
|
||||||
|
t.Errorf("%d %#v except: %s actual: %s", testCase.Number, testCase.Value, testCase.Except, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyObject(t *testing.T) {
|
||||||
|
result := "{}"
|
||||||
|
if b, err := EmptyObjectRawMessage().MarshalJSON(); err == nil {
|
||||||
|
eValue := string(b)
|
||||||
|
if !strings.EqualFold(eValue, result) {
|
||||||
|
t.Errorf("Excepted value: %s, actual value: %s", eValue, result)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf("Error: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyArray(t *testing.T) {
|
||||||
|
result := "[]"
|
||||||
|
if b, err := EmptyArrayRawMessage().MarshalJSON(); err == nil {
|
||||||
|
eValue := string(b)
|
||||||
|
if !strings.EqualFold(eValue, result) {
|
||||||
|
t.Errorf("Excepted value: %s, actual value: %s", eValue, result)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf("Error: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsEmptyRawMessage(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
Number int
|
||||||
|
Value json.RawMessage
|
||||||
|
Empty bool
|
||||||
|
}
|
||||||
|
v1, _ := ToRawMessage([]string{}, "[]")
|
||||||
|
v2, _ := ToRawMessage([]string{"a", "b"}, "[]")
|
||||||
|
v3, _ := ToRawMessage([]int{1, 2, 3}, "[]")
|
||||||
|
v4, _ := ToRawMessage(struct {
|
||||||
|
Name string
|
||||||
|
Age int
|
||||||
|
}{"John", 10}, "{}")
|
||||||
|
v5, _ := ToRawMessage(nil, "[]")
|
||||||
|
a := json.RawMessage{}
|
||||||
|
a.UnmarshalJSON([]byte("null"))
|
||||||
|
b := json.RawMessage{}
|
||||||
|
b.UnmarshalJSON([]byte(""))
|
||||||
|
c := json.RawMessage{}
|
||||||
|
c.UnmarshalJSON([]byte("[ ]"))
|
||||||
|
testCases := []testCase{
|
||||||
|
{1, json.RawMessage{}, true},
|
||||||
|
{2, EmptyObjectRawMessage(), true},
|
||||||
|
{3, EmptyArrayRawMessage(), true},
|
||||||
|
{4, v1, true},
|
||||||
|
{5, v2, false},
|
||||||
|
{6, v3, false},
|
||||||
|
{7, v4, false},
|
||||||
|
{8, v5, true},
|
||||||
|
{9, a, true},
|
||||||
|
{10, b, true},
|
||||||
|
{11, c, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
v := IsEmptyRawMessage(tc.Value)
|
||||||
|
if v != tc.Empty {
|
||||||
|
t.Errorf("%d except: %v, actual: %v", tc.Number, tc.Empty, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvert(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
Number int
|
||||||
|
From json.RawMessage
|
||||||
|
Except any
|
||||||
|
}{
|
||||||
|
{1, nil, struct{}{}},
|
||||||
|
{2, EmptyArrayRawMessage(), []struct{}{}},
|
||||||
|
{3, []byte(`{"ID":1,"Name":"hiscaler"}`), struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
}{}},
|
||||||
|
{4, []byte(`{"ID":1,"Name":"hiscaler","age":1}`), struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
age int
|
||||||
|
}{}},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
exceptValue := testCase.Except
|
||||||
|
err := Convert(testCase.From, &exceptValue)
|
||||||
|
assert.Equalf(t, nil, err, "Test %d", testCase.Number)
|
||||||
|
actualValue := ""
|
||||||
|
if testCase.From != nil {
|
||||||
|
actualValue = ToJson(exceptValue, "null")
|
||||||
|
}
|
||||||
|
t.Logf(`
|
||||||
|
#%d %s
|
||||||
|
↓
|
||||||
|
%#v`, testCase.Number, testCase.From, exceptValue)
|
||||||
|
assert.Equalf(t, string(testCase.From), actualValue, "Test %d", testCase.Number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtract(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
Number int
|
||||||
|
From string
|
||||||
|
Except string
|
||||||
|
HasError bool
|
||||||
|
}{
|
||||||
|
{1, "", "", true},
|
||||||
|
{2, "{}", "{}", false},
|
||||||
|
{3, " {} ", "{}", false},
|
||||||
|
{4, `{"a": 1, "b": 2}`, `{"a": 1, "b": 2}`, false},
|
||||||
|
{5, `{"a": 1, "b": 2}}}}a`, `{"a": 1, "b": 2}`, false},
|
||||||
|
{6, `{"a": 1, "b": 2}}}}<!--This page has been excluded -->`, `{"a": 1, "b": 2}`, false},
|
||||||
|
{7, "[]", "[]", false},
|
||||||
|
{8, "[]1[]2", "[]", false},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
exceptValue := testCase.Except
|
||||||
|
actualValue, err := Extract(testCase.From)
|
||||||
|
assert.Equalf(t, testCase.HasError, err != nil, "Test HasError %d", testCase.Number)
|
||||||
|
assert.Equalf(t, exceptValue, actualValue, "Test Value %d", testCase.Number)
|
||||||
|
}
|
||||||
|
}
|
||||||
246
jsonx/parser.go
Normal file
246
jsonx/parser.go
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
package jsonx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.cloudyne.io/go/hiscaler-gox/bytex"
|
||||||
|
"git.cloudyne.io/go/hiscaler-gox/stringx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parser is a json string parse helper and not required define struct.
|
||||||
|
// You can use Find() method get the path value, and convert to string, int, int64, float32, float64, bool value.
|
||||||
|
// And you can use Exists() method check path is exists
|
||||||
|
// Usage:
|
||||||
|
// parser := jsonx.NewParser("[0,1,2]")
|
||||||
|
// parser.Find("1").Int() // Return 1, founded
|
||||||
|
// parser.Find("10", 0).Int() // Return 0 because not found, you give a default value 0
|
||||||
|
|
||||||
|
type Parser struct {
|
||||||
|
data reflect.Value
|
||||||
|
value reflect.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParseFinder Parser
|
||||||
|
|
||||||
|
func (pf ParseFinder) Interface() interface{} {
|
||||||
|
return pf.value.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pf ParseFinder) String() string {
|
||||||
|
switch pf.value.Kind() {
|
||||||
|
case reflect.Invalid:
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
|
return stringx.String(pf.value.Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pf ParseFinder) Float32() float32 {
|
||||||
|
switch pf.value.Kind() {
|
||||||
|
case reflect.Invalid:
|
||||||
|
return 0
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return float32(pf.value.Int())
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
return float32(pf.value.Uint())
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return float32(pf.value.Float())
|
||||||
|
case reflect.Bool:
|
||||||
|
if pf.value.Bool() {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case reflect.String:
|
||||||
|
d, err := strconv.ParseFloat(pf.value.String(), 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return float32(d)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pf ParseFinder) Float64() float64 {
|
||||||
|
switch pf.value.Kind() {
|
||||||
|
case reflect.Invalid:
|
||||||
|
return 0
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return float64(pf.value.Int())
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
return float64(pf.value.Uint())
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return pf.value.Float()
|
||||||
|
case reflect.Bool:
|
||||||
|
if pf.value.Bool() {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case reflect.String:
|
||||||
|
d, _ := strconv.ParseFloat(pf.value.String(), 64)
|
||||||
|
return d
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pf ParseFinder) Int() int {
|
||||||
|
switch pf.value.Kind() {
|
||||||
|
case reflect.Invalid:
|
||||||
|
return 0
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return int(pf.value.Int())
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
return int(pf.value.Uint())
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return int(pf.value.Float())
|
||||||
|
case reflect.Bool:
|
||||||
|
if pf.value.Bool() {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case reflect.String:
|
||||||
|
d, _ := strconv.Atoi(pf.value.String())
|
||||||
|
return d
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pf ParseFinder) Int64() int64 {
|
||||||
|
switch pf.value.Kind() {
|
||||||
|
case reflect.Invalid:
|
||||||
|
return 0
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return pf.value.Int()
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
return int64(pf.value.Uint())
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return int64(pf.value.Float())
|
||||||
|
case reflect.Bool:
|
||||||
|
if pf.value.Bool() {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case reflect.String:
|
||||||
|
d, err := strconv.ParseInt(pf.value.String(), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pf ParseFinder) Bool() bool {
|
||||||
|
switch pf.value.Kind() {
|
||||||
|
case reflect.Invalid:
|
||||||
|
return false
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return pf.value.Int() > 0
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
return pf.value.Uint() > 0
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return pf.value.Float() > 0
|
||||||
|
case reflect.Bool:
|
||||||
|
return pf.value.Bool()
|
||||||
|
case reflect.String:
|
||||||
|
v, _ := strconv.ParseBool(pf.value.String())
|
||||||
|
return v
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getElement(v reflect.Value, p string) reflect.Value {
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Map:
|
||||||
|
vv := v.MapIndex(reflect.ValueOf(p))
|
||||||
|
if vv.Kind() == reflect.Interface {
|
||||||
|
vv = vv.Elem()
|
||||||
|
}
|
||||||
|
return vv
|
||||||
|
case reflect.Array, reflect.Slice:
|
||||||
|
if i, err := strconv.Atoi(p); err == nil {
|
||||||
|
if i >= 0 && i < v.Len() {
|
||||||
|
v = v.Index(i)
|
||||||
|
for v.Kind() == reflect.Interface {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reflect.Value{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewParser(s string) *Parser {
|
||||||
|
p := &Parser{}
|
||||||
|
return p.LoadString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) LoadString(s string) *Parser {
|
||||||
|
return p.LoadBytes(stringx.ToBytes(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) LoadBytes(bytes []byte) *Parser {
|
||||||
|
if bytex.IsBlank(bytes) {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
var sd interface{}
|
||||||
|
if err := json.Unmarshal(bytes, &sd); err != nil {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
p.data = reflect.ValueOf(sd)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Parser) Exists(path string) bool {
|
||||||
|
if !p.data.IsValid() || path == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
data := p.data
|
||||||
|
parts := strings.Split(path, ".")
|
||||||
|
n := len(parts)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if data = getElement(data, parts[i]); !data.IsValid() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if i == n-1 {
|
||||||
|
// is last path
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) Find(path string, defaultValue ...interface{}) *ParseFinder {
|
||||||
|
if len(defaultValue) > 0 {
|
||||||
|
p.value = reflect.ValueOf(defaultValue[0])
|
||||||
|
}
|
||||||
|
if !p.data.IsValid() || path == "" {
|
||||||
|
return (*ParseFinder)(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := p.data
|
||||||
|
// find the value corresponding to the path
|
||||||
|
// if any part of path cannot be located, return the default value
|
||||||
|
parts := strings.Split(path, ".")
|
||||||
|
n := len(parts)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if data = getElement(data, parts[i]); !data.IsValid() {
|
||||||
|
return (*ParseFinder)(p)
|
||||||
|
}
|
||||||
|
if i == n-1 {
|
||||||
|
// is last path
|
||||||
|
p.value = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (*ParseFinder)(p)
|
||||||
|
}
|
||||||
78
jsonx/parser_test.go
Normal file
78
jsonx/parser_test.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package jsonx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParser_Find(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
json string
|
||||||
|
path string
|
||||||
|
defaultValue interface{}
|
||||||
|
valueKind reflect.Kind
|
||||||
|
Except interface{}
|
||||||
|
}{
|
||||||
|
{"string1", "", "a", "", reflect.String, ""},
|
||||||
|
{"string2", `{"a":1}`, "a", 2, reflect.String, "1"},
|
||||||
|
{"string3", `{"a":true}`, "a", 2, reflect.String, "true"},
|
||||||
|
{"string4", `{"a":true}`, "a.b", false, reflect.String, "false"},
|
||||||
|
{"string5", `{"a":{"b": {"c": 123}}}`, "a.b", "{}", reflect.String, `{"c":123}`},
|
||||||
|
{"string6", `{"a":{"b": {"c": 123}}}`, "a.b.c", "", reflect.String, "123"},
|
||||||
|
{"string7", `{"a":{"b": {"c": [1,2,3]}}}`, "a.b.c.0", "", reflect.String, "1"},
|
||||||
|
{"string8", `{"a":{"b": {"c": [1,2,3]}}}`, "a.b.c.2", "", reflect.String, "3"},
|
||||||
|
{"string9", `{"a":{"b": {"c": [1,2,3]}}}`, "", "110", reflect.String, "110"},
|
||||||
|
{"int1", `{"a":1}`, "a", 2, reflect.Int, 1},
|
||||||
|
{"int2", `{"a":1}`, "aa", 2, reflect.Int, 2},
|
||||||
|
{"int641", `{"a":1}`, "a", 2, reflect.Int64, int64(1)},
|
||||||
|
{"int641", `{"a":1}`, "aa", 2, reflect.Int64, int64(2)},
|
||||||
|
{"bool1", `{"a":true}`, "a", false, reflect.Bool, true},
|
||||||
|
{"bool2", `{"a":true}`, "a.b", false, reflect.Bool, false},
|
||||||
|
{"float321", `{"a":1.23}`, "a", 0, reflect.Float32, float32(1.23)},
|
||||||
|
{"float322", `{"a":1.23}`, "b", 0, reflect.Float32, float32(0)},
|
||||||
|
{"float641", `{"a":1.23}`, "a", 0, reflect.Float64, 1.23},
|
||||||
|
{"float642", `{"a":1.23}`, "b", 0, reflect.Float64, 0.0},
|
||||||
|
{"interface1", `{"a":1.23}`, "b", 0, reflect.Interface, 0},
|
||||||
|
{"interface2", `null`, "b", 0, reflect.Interface, 0},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
var v interface{}
|
||||||
|
switch testCase.valueKind {
|
||||||
|
case reflect.String:
|
||||||
|
v = NewParser(testCase.json).Find(testCase.path, testCase.defaultValue).String()
|
||||||
|
case reflect.Int:
|
||||||
|
v = NewParser(testCase.json).Find(testCase.path, testCase.defaultValue).Int()
|
||||||
|
case reflect.Int64:
|
||||||
|
v = NewParser(testCase.json).Find(testCase.path, testCase.defaultValue).Int64()
|
||||||
|
case reflect.Float32:
|
||||||
|
v = NewParser(testCase.json).Find(testCase.path, testCase.defaultValue).Float32()
|
||||||
|
case reflect.Float64:
|
||||||
|
v = NewParser(testCase.json).Find(testCase.path, testCase.defaultValue).Float64()
|
||||||
|
case reflect.Bool:
|
||||||
|
v = NewParser(testCase.json).Find(testCase.path, testCase.defaultValue).Bool()
|
||||||
|
case reflect.Interface:
|
||||||
|
v = NewParser(testCase.json).Find(testCase.path, testCase.defaultValue).Interface()
|
||||||
|
}
|
||||||
|
assert.Equal(t, testCase.Except, v, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParser_Exists(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
json string
|
||||||
|
path string
|
||||||
|
Except bool
|
||||||
|
}{
|
||||||
|
{"exists1", "", "a", false},
|
||||||
|
{"exists2", `{"a"}`, "a", false},
|
||||||
|
{"exists3", `{"a":1}`, "a", true},
|
||||||
|
{"exists4", `{"a":[0,1,2]}`, "a.1", true},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
v := NewParser(testCase.json).Exists(testCase.path)
|
||||||
|
assert.Equal(t, testCase.Except, v, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
70
keyx/key.go
Normal file
70
keyx/key.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package keyx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generate 生成 Key
|
||||||
|
func Generate(values ...interface{}) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, value := range values {
|
||||||
|
v := reflect.ValueOf(value)
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
if v.Len() != 0 {
|
||||||
|
sb.WriteString(v.String())
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
sb.WriteString(strconv.FormatBool(v.Bool()))
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
if b, ok := value.(rune); ok {
|
||||||
|
sb.WriteRune(b)
|
||||||
|
} else {
|
||||||
|
sb.WriteString(strconv.FormatInt(v.Int(), 10))
|
||||||
|
}
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
if b, ok := value.(byte); ok {
|
||||||
|
sb.WriteByte(b)
|
||||||
|
} else {
|
||||||
|
sb.WriteString(strconv.FormatUint(v.Uint(), 10))
|
||||||
|
}
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
sb.WriteString(strconv.FormatFloat(v.Float(), 'f', -1, 64))
|
||||||
|
case reflect.Map:
|
||||||
|
keys := make([]string, len(v.MapKeys()))
|
||||||
|
i := 0
|
||||||
|
for _, mv := range v.MapKeys() {
|
||||||
|
keys[i] = mv.String()
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
interfaces := make([]interface{}, 0)
|
||||||
|
for k := range keys {
|
||||||
|
interfaces = append(interfaces, keys[k], v.MapIndex(reflect.ValueOf(keys[k])).Interface())
|
||||||
|
}
|
||||||
|
sb.WriteString(Generate(interfaces...))
|
||||||
|
case reflect.Slice, reflect.Array:
|
||||||
|
interfaces := make([]interface{}, 0)
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
interfaces = append(interfaces, v.Index(i).Interface())
|
||||||
|
}
|
||||||
|
sb.WriteString(Generate(interfaces...))
|
||||||
|
case reflect.Struct:
|
||||||
|
kv := map[string]interface{}{}
|
||||||
|
t := reflect.TypeOf(value)
|
||||||
|
if t.Name() != "" {
|
||||||
|
sb.WriteString(t.Name() + ":")
|
||||||
|
}
|
||||||
|
for k := 0; k < t.NumField(); k++ {
|
||||||
|
kv[t.Field(k).Name] = v.Field(k).Interface()
|
||||||
|
}
|
||||||
|
sb.WriteString(Generate(kv))
|
||||||
|
default:
|
||||||
|
sb.WriteString(v.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
63
keyx/key_test.go
Normal file
63
keyx/key_test.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package keyx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerate(t *testing.T) {
|
||||||
|
type User struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
type testCase struct {
|
||||||
|
Number int
|
||||||
|
Values interface{}
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
b1 := []byte("")
|
||||||
|
b2 := []byte("abc")
|
||||||
|
testCases := []testCase{
|
||||||
|
{1, []interface{}{1, 2, 3}, "123"},
|
||||||
|
{2, []interface{}{0, -1, 2, 3}, "0-123"},
|
||||||
|
{3, []interface{}{1.1, 2.12, 3.123}, "1.12.123.123"},
|
||||||
|
{4, []interface{}{1.1, 2.12, 3.123}, "1.12.123.123"},
|
||||||
|
{5, []interface{}{"a", "b", "c"}, "abc"},
|
||||||
|
{6, []interface{}{"a", "b", "c", 1, 2, 3}, "abc123"},
|
||||||
|
{7, []interface{}{true, true, false, false}, "truetruefalsefalse"},
|
||||||
|
{8, []interface{}{[]int{1, 2, 3}}, "123"},
|
||||||
|
{9, []interface{}{[...]int{1, 2, 3, 4}}, "1234"},
|
||||||
|
{10, []interface{}{struct {
|
||||||
|
Username string
|
||||||
|
Age int
|
||||||
|
}{}}, "Age0Username"},
|
||||||
|
{11, []interface{}{struct {
|
||||||
|
Username string
|
||||||
|
Age int
|
||||||
|
}{"John", 12}}, "Age12UsernameJohn"},
|
||||||
|
{12, []interface{}{User{
|
||||||
|
ID: 1,
|
||||||
|
Name: "John",
|
||||||
|
}}, "User:ID1NameJohn"},
|
||||||
|
// byte
|
||||||
|
{13, []interface{}{b1, b2}, "abc"},
|
||||||
|
// rune
|
||||||
|
{14, []interface{}{'a', 'b', 'c'}, "abc"},
|
||||||
|
{15, []map[string]string{{"k1": "v1", "k2": "v2"}}, "k1v1k2v2"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
key := Generate(tc.Values)
|
||||||
|
if key != tc.Key {
|
||||||
|
t.Errorf("%d: except:%s actual:%s", tc.Number, tc.Key, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkGenerate(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
Generate([]interface{}{struct {
|
||||||
|
Username string
|
||||||
|
Age int
|
||||||
|
}{"John", 12}})
|
||||||
|
}
|
||||||
|
}
|
||||||
52
mapx/map.go
Normal file
52
mapx/map.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package mapx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Keys 获取 map 键值(默认按照升序排列)
|
||||||
|
func Keys(m interface{}) []string {
|
||||||
|
var keys []string
|
||||||
|
vo := reflect.ValueOf(m)
|
||||||
|
if vo.Kind() == reflect.Map {
|
||||||
|
mapKeys := vo.MapKeys()
|
||||||
|
keys = make([]string, len(mapKeys))
|
||||||
|
for k, v := range mapKeys {
|
||||||
|
var vString string
|
||||||
|
switch v.Type().Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
vString = strconv.FormatInt(v.Int(), 10)
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
vString = strconv.FormatFloat(v.Float(), 'f', -1, 64)
|
||||||
|
case reflect.Bool:
|
||||||
|
if v.Bool() {
|
||||||
|
vString = "1"
|
||||||
|
} else {
|
||||||
|
vString = "0"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
vString = v.String()
|
||||||
|
}
|
||||||
|
keys[k] = vString
|
||||||
|
}
|
||||||
|
if len(keys) > 0 {
|
||||||
|
sort.Strings(keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringMapStringEncode(params map[string]string) string {
|
||||||
|
if len(params) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
values := url.Values{}
|
||||||
|
for k, v := range params {
|
||||||
|
values.Add(k, v)
|
||||||
|
}
|
||||||
|
return values.Encode()
|
||||||
|
}
|
||||||
60
mapx/map_test.go
Normal file
60
mapx/map_test.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package mapx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKeys(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
value interface{}
|
||||||
|
keys []string
|
||||||
|
}{
|
||||||
|
{"t0", nil, nil},
|
||||||
|
{"t1", map[string]interface{}{"a": 1, "b": 2}, []string{"a", "b"}},
|
||||||
|
{"t2", map[string]interface{}{"b": 1, "a": 2}, []string{"a", "b"}},
|
||||||
|
{"t3", map[string]interface{}{"a": 1, "b": 2, "": 3}, []string{"", "a", "b"}},
|
||||||
|
{"t4", map[string]string{"a": "1", "b": "2", "": "3"}, []string{"", "a", "b"}},
|
||||||
|
{"t4", map[int]string{1: "1", 3: "3", 2: "2"}, []string{"1", "2", "3"}},
|
||||||
|
{"t4", map[float64]string{1.1: "1", 3: "3", 2: "2"}, []string{"1.1", "2", "3"}},
|
||||||
|
{"t4", map[bool]string{true: "1", false: "3"}, []string{"0", "1"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
keys := Keys(test.value)
|
||||||
|
v := assert.Equal(t, test.keys, keys, test.tag)
|
||||||
|
if v {
|
||||||
|
for k, value := range test.keys {
|
||||||
|
assert.Equal(t, value, keys[k], fmt.Sprintf("keys[%d]", k))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkKeys(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
Keys(map[interface{}]interface{}{"a": 1, "b": 2, "c": "cValue", "d": "dValue", 1: 1, 2: 2})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringMapStringEncode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
value map[string]string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t0", nil, ""},
|
||||||
|
{"t1", map[string]string{"a": "1", "b": "2"}, "a=1&b=2"},
|
||||||
|
{"t2", map[string]string{"b": "1", "a": "2"}, "a=2&b=1"},
|
||||||
|
{"t3", map[string]string{"a": "1", "b": "2", "c": "3"}, "a=1&b=2&c=3"},
|
||||||
|
{"t4", map[string]string{"a": "1", "b": "2", "": "3"}, "=3&a=1&b=2"},
|
||||||
|
{"t4", map[string]string{"1": "1", "3": "3", "2": "2"}, "1=1&2=2&3=3"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
s := StringMapStringEncode(test.value)
|
||||||
|
assert.Equal(t, test.expected, s, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
91
net/urlx/url.go
Normal file
91
net/urlx/url.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package urlx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.cloudyne.io/go/hiscaler-gox/isx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type URL struct {
|
||||||
|
Path string // URL path
|
||||||
|
URL *url.URL // A url.URL represents
|
||||||
|
Invalid bool // Path is a valid url
|
||||||
|
values url.Values // Query values
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewURL(path string) *URL {
|
||||||
|
u := &URL{
|
||||||
|
Path: path,
|
||||||
|
Invalid: false,
|
||||||
|
values: url.Values{},
|
||||||
|
}
|
||||||
|
if v, err := url.Parse(u.Path); err == nil {
|
||||||
|
u.URL = v
|
||||||
|
u.Invalid = true
|
||||||
|
if values, err := url.ParseQuery(v.RawQuery); err == nil {
|
||||||
|
u.values = values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u URL) GetValue(key, defaultValue string) string {
|
||||||
|
v := u.values.Get(key)
|
||||||
|
if v == "" {
|
||||||
|
v = defaultValue
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u URL) SetValue(key, value string) URL {
|
||||||
|
u.values.Set(key, value)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u URL) AddValue(key, value string) URL {
|
||||||
|
u.values.Add(key, value)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u URL) DelKey(key string) URL {
|
||||||
|
u.values.Del(key)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u URL) HasKey(key string) bool {
|
||||||
|
return u.values.Has(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u URL) String() string {
|
||||||
|
s := u.URL.String()
|
||||||
|
rawQuery := u.URL.RawQuery
|
||||||
|
if rawQuery == "" {
|
||||||
|
if len(u.values) > 0 {
|
||||||
|
s += "?" + u.values.Encode()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s = strings.Replace(s, rawQuery, u.values.Encode(), 1)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAbsolute 是否为绝对地址
|
||||||
|
func IsAbsolute(s string) bool {
|
||||||
|
if strings.HasPrefix(s, "//") {
|
||||||
|
s = "http:" + s
|
||||||
|
}
|
||||||
|
if isx.HttpURL(s) {
|
||||||
|
if u, err := url.Parse(s); err == nil {
|
||||||
|
if u.IsAbs() && len(u.Host) > 2 && strings.Index(u.Host, ".") > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRelative 是否为相对地址
|
||||||
|
func IsRelative(url string) bool {
|
||||||
|
return !IsAbsolute(url)
|
||||||
|
}
|
||||||
88
net/urlx/url_test.go
Normal file
88
net/urlx/url_test.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package urlx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestURL_AddValue(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
Number int
|
||||||
|
Path string
|
||||||
|
Values map[string]string
|
||||||
|
Except string
|
||||||
|
}
|
||||||
|
testCases := []testCase{
|
||||||
|
{1, "https://www.example.com/a/b/c/1.txt?a=1&b=2", map[string]string{"a": "11", "b": "22"}, "https://www.example.com/a/b/c/1.txt?a=11&b=22"},
|
||||||
|
{1, "https://www.example.com/a/b/c/1.txt?a=1&b=2&c=3", map[string]string{"a": "11", "c": "33"}, "https://www.example.com/a/b/c/1.txt?a=11&b=2&c=33"},
|
||||||
|
{2, "https://www.example.com/a/b/c/1.txt?a=1&b=2#abc", map[string]string{"a": "11"}, "https://www.example.com/a/b/c/1.txt?a=11&b=2#abc"},
|
||||||
|
{3, "https://www.example.com/a/b/c/1.txt?a=1&b=2#abc", map[string]string{"A": "11"}, "https://www.example.com/a/b/c/1.txt?A=11&a=1&b=2#abc"},
|
||||||
|
{4, "https://www.example.com/a/b/c/1.txt?b=1&a=2#abc", map[string]string{"A": "11"}, "https://www.example.com/a/b/c/1.txt?A=11&a=2&b=1#abc"},
|
||||||
|
{5, "https://www.example.com", map[string]string{"A": "11"}, "https://www.example.com?A=11"},
|
||||||
|
{6, "https://www.example.com/", map[string]string{"A": "11"}, "https://www.example.com/?A=11"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
url := NewURL(tc.Path)
|
||||||
|
for k, v := range tc.Values {
|
||||||
|
url.SetValue(k, v)
|
||||||
|
}
|
||||||
|
s := url.String()
|
||||||
|
if s != tc.Except {
|
||||||
|
t.Errorf("%d except: %s, actual: %s", tc.Number, tc.Except, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestURL_DeleteValue(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
Number int
|
||||||
|
Path string
|
||||||
|
DeleteKeys []string
|
||||||
|
Except string
|
||||||
|
}
|
||||||
|
testCases := []testCase{
|
||||||
|
{1, "https://www.example.com/a/b/c/1.txt?a=1&b=2#abc", []string{"a", "b"}, "https://www.example.com/a/b/c/1.txt?#abc"},
|
||||||
|
{1, "https://www.example.com/a/b/c/1.txt?a=1&b=2#abc", []string{"a"}, "https://www.example.com/a/b/c/1.txt?b=2#abc"},
|
||||||
|
{2, "https://www.example.com/a/b/c/1.txt", []string{"a", "b"}, "https://www.example.com/a/b/c/1.txt"},
|
||||||
|
{2, "https://www/a/b/c/1.txt", []string{"a", "b"}, "https://www/a/b/c/1.txt"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
url := NewURL(tc.Path)
|
||||||
|
for _, v := range tc.DeleteKeys {
|
||||||
|
url.DelKey(v)
|
||||||
|
}
|
||||||
|
s := url.String()
|
||||||
|
if s != tc.Except {
|
||||||
|
t.Errorf("%d except: %s, actual: %s", tc.Number, tc.Except, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsAbsolute(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
url string
|
||||||
|
isAbs bool
|
||||||
|
}{
|
||||||
|
{"t0.1", "https://www.a.com", true},
|
||||||
|
{"t0.2", "http://www.a.com", true},
|
||||||
|
{"t0.3", "//www.a.com", true},
|
||||||
|
{"t0.4", "//a.b", true},
|
||||||
|
{"t0.5", "//abc", false},
|
||||||
|
{"t0.6", "//abc...", false},
|
||||||
|
{"t0.7", "//.a.b", false},
|
||||||
|
{"t0.8", "//a.b..", false},
|
||||||
|
|
||||||
|
{"t1.1", "httpa.com", false},
|
||||||
|
{"t1.2", "httpa.com//", false},
|
||||||
|
{"t1.3", "//", false},
|
||||||
|
{"t1.4", "//a", false},
|
||||||
|
{"t1.5", "//....a", false},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
isAbs := IsAbsolute(testCase.url)
|
||||||
|
assert.Equal(t, testCase.isAbs, isAbs, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
nullx/string.go
Normal file
18
nullx/string.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package nullx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gopkg.in/guregu/null.v4"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StringFrom(s string) null.String {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return NullString()
|
||||||
|
}
|
||||||
|
return null.NewString(s, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NullString() null.String {
|
||||||
|
return null.NewString("", false)
|
||||||
|
}
|
||||||
17
nullx/time.go
Normal file
17
nullx/time.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package nullx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gopkg.in/guregu/null.v4"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TimeFrom(t time.Time) null.Time {
|
||||||
|
if t.IsZero() {
|
||||||
|
return NullTime()
|
||||||
|
}
|
||||||
|
return null.TimeFrom(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NullTime() null.Time {
|
||||||
|
return null.NewTime(time.Time{}, false)
|
||||||
|
}
|
||||||
18
pathx/path.go
Normal file
18
pathx/path.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package pathx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FilenameWithoutExt(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := path.Base(s)
|
||||||
|
if ext := path.Ext(s); ext != "" {
|
||||||
|
filename = strings.TrimSuffix(filename, ext)
|
||||||
|
}
|
||||||
|
return filename
|
||||||
|
}
|
||||||
25
pathx/path_test.go
Normal file
25
pathx/path_test.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package pathx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFilenameWithoutExt(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
path string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t1", "a.jpg", "a"},
|
||||||
|
{"t2", "/a/b/c.jpg", "c"},
|
||||||
|
{"t3", "/a/b/c", "c"},
|
||||||
|
{"t4", "/a/b/c/", "c"},
|
||||||
|
{"t5", "/a/b/c/中文.jpg", "中文"},
|
||||||
|
{"t5", "https://www.example.com/a/b/c/中文.jpg", "中文"},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
v := FilenameWithoutExt(testCase.path)
|
||||||
|
assert.Equal(t, testCase.expected, v, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
46
randx/rand.go
Normal file
46
randx/rand.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package randx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
randNumberChars = "0123456789"
|
||||||
|
randLetterChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateValues(str string, n int, upper bool) string {
|
||||||
|
if n <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
sb := strings.Builder{}
|
||||||
|
sb.Grow(n)
|
||||||
|
bigInt := big.NewInt(int64(len(str)))
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
randomInt, _ := rand.Int(rand.Reader, bigInt)
|
||||||
|
sb.WriteByte(str[randomInt.Int64()])
|
||||||
|
}
|
||||||
|
s := sb.String()
|
||||||
|
if upper {
|
||||||
|
s = strings.ToUpper(s)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Letter Generate letter rand string
|
||||||
|
func Letter(n int, upper bool) string {
|
||||||
|
return generateValues(randLetterChars, n, upper)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number Generate number rand string
|
||||||
|
func Number(n int) string {
|
||||||
|
return generateValues(randNumberChars, n, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any Generate number and letter combined string
|
||||||
|
func Any(n int) string {
|
||||||
|
return generateValues(randLetterChars+randNumberChars, n, false)
|
||||||
|
}
|
||||||
17
randx/rand_test.go
Normal file
17
randx/rand_test.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package randx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkNumber(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
Number(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAny(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
Any(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
70
setx/set.go
Normal file
70
setx/set.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package setx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
gox "git.cloudyne.io/go/hiscaler-gox"
|
||||||
|
"git.cloudyne.io/go/hiscaler-gox/inx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToSet change slice to unique values
|
||||||
|
func ToSet[T gox.Number | string | bool | byte | rune](values []T) []T {
|
||||||
|
if len(values) <= 1 {
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqueValues := make([]T, 0)
|
||||||
|
kv := make(map[T]struct{}, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
if _, ok := kv[value]; !ok {
|
||||||
|
kv[value] = struct{}{}
|
||||||
|
uniqueValues = append(uniqueValues, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqueValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToStringSet(values []string, caseSensitive bool) []string {
|
||||||
|
if len(values) <= 1 {
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
m := make(map[string]string, 0)
|
||||||
|
for _, value := range values {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value != "" {
|
||||||
|
fixedValue := value
|
||||||
|
if !caseSensitive {
|
||||||
|
fixedValue = strings.ToLower(fixedValue)
|
||||||
|
}
|
||||||
|
if _, ok := m[fixedValue]; !ok {
|
||||||
|
m[fixedValue] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(m) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sets := make([]string, len(m))
|
||||||
|
i := 0
|
||||||
|
for _, v := range m {
|
||||||
|
sets[i] = v
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return sets
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToIntSet(values []int) []int {
|
||||||
|
if len(values) <= 1 {
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
sets := make([]int, 0)
|
||||||
|
for _, value := range values {
|
||||||
|
if !inx.IntIn(value, sets...) {
|
||||||
|
sets = append(sets, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sets
|
||||||
|
}
|
||||||
92
setx/set_test.go
Normal file
92
setx/set_test.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package setx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestToSet(t *testing.T) {
|
||||||
|
assert.ElementsMatch(t, []int{1, 2, 3}, ToSet([]int{1, 2, 3}), "int1")
|
||||||
|
assert.ElementsMatch(t, []int{1, 2, 3}, ToSet([]int{1, 1, 2, 2, 3, 3}), "int2")
|
||||||
|
assert.ElementsMatch(t, []float32{1, 2, 3}, ToSet([]float32{1, 2, 3}), "float321")
|
||||||
|
assert.ElementsMatch(t, []float64{1, 2, 3}, ToSet([]float64{1, 1, 2, 2, 3, 3}), "float641")
|
||||||
|
assert.ElementsMatch(t, []string{"A", "B", "C"}, ToSet([]string{"A", "B", "C", "C", "B", "A"}), "string1")
|
||||||
|
assert.ElementsMatch(t, []string{"A", " A ", "B", "C"}, ToSet([]string{" A ", "B", "C", "C", "B", "A"}), "string2")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToStringSet(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
A []string
|
||||||
|
B []string
|
||||||
|
Len int
|
||||||
|
Values []string
|
||||||
|
}{
|
||||||
|
{[]string{"1", "2", "3"}, []string{"0", "1", "4"}, 5, []string{"0", "1", "2", "3", "4"}},
|
||||||
|
{[]string{"1", "1", "1"}, []string{"0", "1", "2"}, 3, []string{"0", "1", "2"}},
|
||||||
|
{[]string{" ", "1", "1", "1"}, []string{"0", "1", "2"}, 3, []string{"0", "1", "2"}},
|
||||||
|
{[]string{"\tabc\t", " abc ", "1", "1", "1"}, []string{"0", "1", "2"}, 4, []string{"0", "1", "2", "abc"}},
|
||||||
|
{[]string{"\tabc\t", " abc ", "1", "1", "1", "ABC"}, []string{"0", "1", "2"}, 5, []string{"0", "1", "2", "abc", "ABC"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
c := ToStringSet(append(testCase.A, testCase.B...), true)
|
||||||
|
if len(c) != testCase.Len {
|
||||||
|
t.Errorf("Except %d, actual %d", testCase.Len, len(c))
|
||||||
|
}
|
||||||
|
for _, value := range testCase.Values {
|
||||||
|
exists := false
|
||||||
|
for _, v := range c {
|
||||||
|
if v == value {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
t.Errorf("%s not in %#v", value, testCase.Values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkToStringSet(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
ToStringSet([]string{"A", "B", "c", "C", "a", "d", "d", "e", "fgh", "FGH", "fGH", "fgH"}, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntSliceToSet(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
A []int
|
||||||
|
B []int
|
||||||
|
Len int
|
||||||
|
Values []int
|
||||||
|
}{
|
||||||
|
{[]int{1, 2, 3}, []int{0, 1, 4}, 5, []int{0, 1, 2, 3, 4}},
|
||||||
|
{[]int{1, 1, 1}, []int{0, 1, 2}, 3, []int{0, 1, 2}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
c := ToIntSet(append(testCase.A, testCase.B...))
|
||||||
|
if len(c) != testCase.Len {
|
||||||
|
t.Errorf("Except %d, actual %d", testCase.Len, len(c))
|
||||||
|
}
|
||||||
|
for _, value := range testCase.Values {
|
||||||
|
exists := false
|
||||||
|
for _, v := range c {
|
||||||
|
if v == value {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
t.Errorf("%d not in %#v", value, testCase.Values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkToIntSet(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
ToIntSet([]int{1, 2, 3, 3, 45, 5, 6, 56, 56, 56, 77, 6, 7, 67, 678, 78, 78, 8, 78})
|
||||||
|
}
|
||||||
|
}
|
||||||
241
slicex/slice.go
Normal file
241
slicex/slice.go
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
package slicex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
gox "git.cloudyne.io/go/hiscaler-gox"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Map values all value execute f function, and return a new slice
|
||||||
|
//
|
||||||
|
// Example
|
||||||
|
//
|
||||||
|
// Map([]string{"A", "B", "C"}, func(v string) string {
|
||||||
|
// return strings.ToLower(v)
|
||||||
|
// })
|
||||||
|
// // Output: ["a", "b", "c"]
|
||||||
|
func Map[T comparable](values []T, f func(v T) T) []T {
|
||||||
|
items := make([]T, len(values))
|
||||||
|
for i, v := range values {
|
||||||
|
items[i] = f(v)
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter return matched f function condition value
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// Filter([]int{0, 1, 2, 3}, func(v int) bool {
|
||||||
|
// return v > 0
|
||||||
|
// })
|
||||||
|
// // Output: [1, 2, 3]
|
||||||
|
func Filter[T comparable](values []T, f func(v T) bool) []T {
|
||||||
|
items := make([]T, 0)
|
||||||
|
for _, v := range values {
|
||||||
|
if ok := f(v); ok {
|
||||||
|
items = append(items, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToInterface[T gox.Number | ~string](values []T) []interface{} {
|
||||||
|
if values == nil || len(values) == 0 {
|
||||||
|
return []interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ifs := make([]interface{}, len(values))
|
||||||
|
for i, value := range values {
|
||||||
|
ifs[i] = value
|
||||||
|
}
|
||||||
|
return ifs
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringToInterface Change string slice to interface slice
|
||||||
|
func StringToInterface(values []string) []interface{} {
|
||||||
|
return ToInterface(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntToInterface Change int slice to interface slice
|
||||||
|
func IntToInterface(values []int) []interface{} {
|
||||||
|
return ToInterface(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringSliceEqual Check a, b is equal
|
||||||
|
func StringSliceEqual(a, b []string, caseSensitive, ignoreEmpty, trim bool) bool {
|
||||||
|
if a == nil && b == nil {
|
||||||
|
return true
|
||||||
|
} else if a == nil || b == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !caseSensitive || ignoreEmpty || trim {
|
||||||
|
fixFunc := func(ss []string) []string {
|
||||||
|
if len(ss) == 0 {
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
values := make([]string, 0)
|
||||||
|
for _, s := range ss {
|
||||||
|
if trim {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
}
|
||||||
|
if s == "" && ignoreEmpty {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !caseSensitive {
|
||||||
|
s = strings.ToUpper(s)
|
||||||
|
}
|
||||||
|
values = append(values, s)
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
a = fixFunc(a)
|
||||||
|
b = fixFunc(b)
|
||||||
|
}
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, av := range a {
|
||||||
|
exists := false
|
||||||
|
for _, bv := range b {
|
||||||
|
if av == bv {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntSliceEqual Check a, b is equal
|
||||||
|
func IntSliceEqual(a, b []int) bool {
|
||||||
|
if a == nil && b == nil {
|
||||||
|
return true
|
||||||
|
} else if a == nil || b == nil || len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, av := range a {
|
||||||
|
exists := false
|
||||||
|
for _, bv := range b {
|
||||||
|
if av == bv {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringSliceReverse(ss []string) []string {
|
||||||
|
n := len(ss)
|
||||||
|
if n <= 1 {
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
vv := make([]string, len(ss))
|
||||||
|
copy(vv, ss)
|
||||||
|
for k1 := 0; k1 < n/2; k1++ {
|
||||||
|
k2 := n - k1 - 1
|
||||||
|
vv[k1], vv[k2] = vv[k2], vv[k1]
|
||||||
|
}
|
||||||
|
return vv
|
||||||
|
}
|
||||||
|
|
||||||
|
func IntSliceReverse(ss []int) []int {
|
||||||
|
n := len(ss)
|
||||||
|
if n <= 1 {
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
vv := make([]int, len(ss))
|
||||||
|
copy(vv, ss)
|
||||||
|
for k1 := 0; k1 < n/2; k1++ {
|
||||||
|
k2 := n - k1 - 1
|
||||||
|
vv[k1], vv[k2] = vv[k2], vv[k1]
|
||||||
|
}
|
||||||
|
return vv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diff return a slice in ss[0] and not in ss[1:]
|
||||||
|
func Diff[T comparable](values ...[]T) []T {
|
||||||
|
diffValues := make([]T, 0)
|
||||||
|
n := len(values)
|
||||||
|
if n == 0 || values[0] == nil {
|
||||||
|
return diffValues
|
||||||
|
} else if n == 1 {
|
||||||
|
return values[0]
|
||||||
|
} else {
|
||||||
|
items := make(map[T]struct{}, 0)
|
||||||
|
for _, vs := range values[1:] {
|
||||||
|
for _, v := range vs {
|
||||||
|
items[v] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, v := range values[0] {
|
||||||
|
if _, ok := items[v]; !ok {
|
||||||
|
diffValues = append(diffValues, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return diffValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringSliceDiff(ss ...[]string) []string {
|
||||||
|
diffValues := make([]string, 0)
|
||||||
|
if len(ss) == 0 || ss[0] == nil {
|
||||||
|
return diffValues
|
||||||
|
} else if len(ss) == 1 {
|
||||||
|
return ss[0]
|
||||||
|
} else {
|
||||||
|
for _, v1 := range ss[0] {
|
||||||
|
exists := false
|
||||||
|
for _, items := range ss[1:] {
|
||||||
|
for _, v2 := range items {
|
||||||
|
if strings.EqualFold(v1, v2) {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
diffValues = append(diffValues, v1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return diffValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func IntSliceDiff(ss ...[]int) []int {
|
||||||
|
return Diff(ss...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunk chunks a slice by size
|
||||||
|
func Chunk[T comparable](items []T, size int) [][]T {
|
||||||
|
chunkItems := make([][]T, 0)
|
||||||
|
n := len(items)
|
||||||
|
if items == nil || n == 0 {
|
||||||
|
return chunkItems
|
||||||
|
} else if size <= 0 {
|
||||||
|
return [][]T{items}
|
||||||
|
}
|
||||||
|
for i := 0; i < n; i += size {
|
||||||
|
end := i + size
|
||||||
|
if end > n {
|
||||||
|
end = n
|
||||||
|
}
|
||||||
|
chunkItems = append(chunkItems, items[i:end])
|
||||||
|
}
|
||||||
|
return chunkItems
|
||||||
|
}
|
||||||
219
slicex/slice_test.go
Normal file
219
slicex/slice_test.go
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
package slicex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMap(t *testing.T) {
|
||||||
|
a := []int{0, 1}
|
||||||
|
b := Map(a, func(v int) int {
|
||||||
|
return v + 1
|
||||||
|
})
|
||||||
|
assert.Equal(t, []int{0, 1}, a, "map.int")
|
||||||
|
assert.Equal(t, []int{1, 2}, b, "map.int")
|
||||||
|
|
||||||
|
a1 := []string{"a", "b"}
|
||||||
|
b1 := Map(a1, func(v string) string {
|
||||||
|
return v + "1"
|
||||||
|
})
|
||||||
|
assert.Equal(t, []string{"a", "b"}, a1, "map.string")
|
||||||
|
assert.Equal(t, []string{"a1", "b1"}, b1, "map.string")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilter(t *testing.T) {
|
||||||
|
assert.Equal(t, []int{1, 2, 3}, Filter([]int{0, 1, 2, 3}, func(v int) bool {
|
||||||
|
return v > 0
|
||||||
|
}), "map.int")
|
||||||
|
assert.Equal(t, []string{"c"}, Filter([]string{"a", "b", "c", "a", "b"}, func(v string) bool {
|
||||||
|
return v == "c"
|
||||||
|
}), "map.string")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringToInterface(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
input []string
|
||||||
|
expected []interface{}
|
||||||
|
}{
|
||||||
|
{"t0", []string{"a", "b", "c"}, []interface{}{"a", "b", "c"}},
|
||||||
|
{"t1", nil, []interface{}{}},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
v := StringToInterface(test.input)
|
||||||
|
assert.Equal(t, test.expected, v, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkStringToInterface(b *testing.B) {
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
StringToInterface([]string{"a", "b", "c"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringSliceEqual(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
A []string
|
||||||
|
B []string
|
||||||
|
CaseSensitive bool
|
||||||
|
IgnoreEmpty bool
|
||||||
|
Trim bool
|
||||||
|
Except bool
|
||||||
|
}{
|
||||||
|
{[]string{}, []string{}, false, true, true, true},
|
||||||
|
{[]string{"a", "b", "c"}, []string{"a", "b", "c"}, true, false, true, true},
|
||||||
|
{[]string{"a", "b", "c"}, []string{"a", "b ", " c"}, true, false, true, true},
|
||||||
|
{[]string{"a", "b", "c"}, []string{"a", "b ", " c"}, true, false, false, false},
|
||||||
|
{[]string{"a", "b", "c", ""}, []string{"a", "b ", " c"}, true, false, true, false},
|
||||||
|
{[]string{"a", "b", "c", ""}, []string{"a", "b ", " c"}, true, true, true, true},
|
||||||
|
{[]string{"a", "b", "c", ""}, []string{"a", "b ", " c", ""}, true, false, true, true},
|
||||||
|
{[]string{"A", "B", "C"}, []string{"a", "b", "c"}, true, true, true, false},
|
||||||
|
{[]string{"A", "B", "C"}, []string{"a", "b", "c"}, false, true, true, true},
|
||||||
|
{[]string{"A", "B", "C"}, []string{"b", "c", "a"}, false, true, true, true},
|
||||||
|
{[]string{" ", "", " "}, []string{""}, false, true, true, true},
|
||||||
|
{[]string{}, []string{" ", ""}, false, true, true, true},
|
||||||
|
{[]string{}, []string{"a", "b"}, false, true, true, false},
|
||||||
|
{nil, []string{}, false, true, true, false},
|
||||||
|
{[]string{}, nil, false, true, true, false},
|
||||||
|
{nil, nil, false, true, true, true},
|
||||||
|
}
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
equal := StringSliceEqual(testCase.A, testCase.B, testCase.CaseSensitive, testCase.IgnoreEmpty, testCase.Trim)
|
||||||
|
if equal != testCase.Except {
|
||||||
|
t.Errorf("%d except %v actual %v", i, testCase.Except, equal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntSliceEqual(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
A []int
|
||||||
|
B []int
|
||||||
|
Except bool
|
||||||
|
}{
|
||||||
|
{[]int{}, []int{}, true},
|
||||||
|
{[]int{0, 1, 2}, []int{0, 1, 2}, true},
|
||||||
|
{[]int{0, 1, 2}, []int{2, 1, 0}, true},
|
||||||
|
{[]int{0, 1, 2}, []int{1, 2}, false},
|
||||||
|
{[]int{0, 1, 1, 2}, []int{0, 1, 2}, false},
|
||||||
|
{[]int{0, 1, 1, 2}, []int{0, 1, 2, 1}, true},
|
||||||
|
{nil, []int{}, false},
|
||||||
|
{nil, nil, true},
|
||||||
|
{[]int{}, nil, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
equal := IntSliceEqual(testCase.A, testCase.B)
|
||||||
|
if equal != testCase.Except {
|
||||||
|
t.Errorf("%d except %v actual %v", i, testCase.Except, equal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringSliceReverse(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
Before []string
|
||||||
|
After []string
|
||||||
|
}{
|
||||||
|
{[]string{"a"}, []string{"a"}},
|
||||||
|
{[]string{"a", "b"}, []string{"b", "a"}},
|
||||||
|
{[]string{"a", "b", "c"}, []string{"c", "b", "a"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
values := StringSliceReverse(testCase.Before)
|
||||||
|
if len(values) != len(testCase.After) {
|
||||||
|
t.Errorf("%#v reverse after value except: %#v, actual: %#v", testCase.Before, testCase.After, values)
|
||||||
|
} else {
|
||||||
|
for j, v := range values {
|
||||||
|
if testCase.After[j] != v {
|
||||||
|
t.Errorf("%#v reverse after value except: %#v, actual: %#v", testCase.Before, testCase.After, values)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringSliceDiff(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
Number int
|
||||||
|
OriginalValues [][]string
|
||||||
|
DiffValue []string
|
||||||
|
}{
|
||||||
|
{1, [][]string{{"a", "b", "c"}, {"a", "b", "d"}}, []string{"c"}},
|
||||||
|
{1, [][]string{{"a", "b", "c"}, {"a"}}, []string{"b", "c"}},
|
||||||
|
{2, [][]string{{"a", "b", "d"}, {"a", "b", "c"}}, []string{"d"}},
|
||||||
|
{3, [][]string{{"a", "b", "c"}, {"a", "b", "c"}}, []string{}},
|
||||||
|
{4, [][]string{{"a", "b", ""}, {"a", "b", "c"}}, []string{""}},
|
||||||
|
{5, [][]string{{"a", "b", "c"}, {"a", "b"}, {"c"}}, []string{}},
|
||||||
|
{6, [][]string{{"a"}, {"b"}, {"c", "c1"}, {"d"}}, []string{"a"}},
|
||||||
|
{7, [][]string{nil, {"a"}, {"b"}, {"c", "c1"}, {"d"}}, []string{}},
|
||||||
|
{8, [][]string{nil}, []string{}},
|
||||||
|
{9, [][]string{nil, nil, nil}, []string{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
values := StringSliceDiff(testCase.OriginalValues...)
|
||||||
|
if !StringSliceEqual(values, testCase.DiffValue, true, false, true) {
|
||||||
|
t.Errorf("%d: diff values except: %#v, actual: %#v", testCase.Number, testCase.DiffValue, values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntSliceDiff(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
Number int
|
||||||
|
OriginalValues [][]int
|
||||||
|
DiffValue []int
|
||||||
|
}{
|
||||||
|
{1, [][]int{{1, 2, 3}, {1, 2, 4}}, []int{3}},
|
||||||
|
{2, [][]int{{1, 2, 3}, {1, 2, 2, 3}, {3, 4, 5}}, []int{}},
|
||||||
|
{3, [][]int{{1, 2, 3}, {1}, {2}, {3}}, []int{}},
|
||||||
|
{4, [][]int{{1, 2, 3}, {1, 2, 4, 0, 2, 1}}, []int{3}},
|
||||||
|
{5, [][]int{{1, 2, 2, 3}, {1}}, []int{2, 2, 3}},
|
||||||
|
{6, [][]int{}, []int{}},
|
||||||
|
{7, [][]int{nil, {1, 2, 3}}, []int{}},
|
||||||
|
{8, [][]int{nil, nil, {1, 2, 3}}, []int{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
values := IntSliceDiff(testCase.OriginalValues...)
|
||||||
|
if !IntSliceEqual(values, testCase.DiffValue) {
|
||||||
|
t.Errorf("%d: diff values except: %#v, actual: %#v", testCase.Number, testCase.DiffValue, values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToInterface(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
values interface{}
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []interface{}
|
||||||
|
}{
|
||||||
|
{"t1", args{[]int{1, 2, 3}}, []interface{}{1, 2, 3}},
|
||||||
|
{"t2", args{[]string{"A", "B", "C"}}, []interface{}{"A", "B", "C"}},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var value interface{}
|
||||||
|
switch tt.args.values.(type) {
|
||||||
|
case []string:
|
||||||
|
value = ToInterface(tt.args.values.([]string))
|
||||||
|
case []int:
|
||||||
|
value = ToInterface(tt.args.values.([]int))
|
||||||
|
}
|
||||||
|
assert.Equalf(t, tt.want, value, "ToInterface(%v)", tt.args.values)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChunk(t *testing.T) {
|
||||||
|
assert.ElementsMatch(t, Chunk([]int{1, 2, 3}, 0), [][]int{{1, 2, 3}}, "int0")
|
||||||
|
assert.ElementsMatch(t, Chunk([]int{1, 2, 3}, 1), [][]int{{1}, {2}, {3}}, "int1")
|
||||||
|
assert.ElementsMatch(t, Chunk([]int{1, 2, 3}, 2), [][]int{{1, 2}, {3}}, "int2")
|
||||||
|
assert.ElementsMatch(t, Chunk([]int{1, 2, 3}, 3), [][]int{{1, 2, 3}}, "int3")
|
||||||
|
}
|
||||||
192
spreedsheetx/column.go
Normal file
192
spreedsheetx/column.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
package spreedsheetx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://support.microsoft.com/en-us/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3?ui=en-us&rs=en-us&ad=us
|
||||||
|
// Max index is 16384 XFD
|
||||||
|
|
||||||
|
var (
|
||||||
|
rxColumnName = regexp.MustCompile("^[A-Za-z]{1,3}$")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
minNumber = 1
|
||||||
|
maxNumber = 16384
|
||||||
|
a = 64
|
||||||
|
)
|
||||||
|
|
||||||
|
type Column struct {
|
||||||
|
startName string // 最开始操作的列
|
||||||
|
endName string // 最远到达的列
|
||||||
|
current string // 当前列
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidName(name string) bool {
|
||||||
|
return rxColumnName.MatchString(name) && toNumber(name) <= maxNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
func reverse(name string) []rune {
|
||||||
|
d := []rune(name)
|
||||||
|
for i, j := 0, len(d)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
d[i], d[j] = d[j], d[i]
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func toNumber(name string) int {
|
||||||
|
name = strings.ToUpper(name)
|
||||||
|
switch len(name) {
|
||||||
|
case 0:
|
||||||
|
return 0
|
||||||
|
case 1:
|
||||||
|
return int(rune(name[0])) - a
|
||||||
|
default:
|
||||||
|
number := 0
|
||||||
|
for i, r := range reverse(name) {
|
||||||
|
if i == 0 {
|
||||||
|
number += int(r) - a
|
||||||
|
} else {
|
||||||
|
number += (int(r) - a) * int(math.Pow(26, float64(i)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewColumn(name string) *Column {
|
||||||
|
name = strings.ToUpper(name)
|
||||||
|
if !isValidName(name) {
|
||||||
|
panic("invalid column name")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Column{
|
||||||
|
startName: name,
|
||||||
|
endName: name,
|
||||||
|
current: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToFirst 到第一列,总是返回 A 列
|
||||||
|
func (c *Column) ToFirst() *Column {
|
||||||
|
c.current = "A"
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next 当前列的下一列
|
||||||
|
func (c *Column) Next() (*Column, error) {
|
||||||
|
return c.RightShift(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Column) Prev() (*Column, error) {
|
||||||
|
return c.LeftShift(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartName 返回最开始的列名
|
||||||
|
func (c Column) StartName() string {
|
||||||
|
return c.startName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Column) setEndName(name string) *Column {
|
||||||
|
if c.endName < name || len(c.endName) < len(name) {
|
||||||
|
c.endName = name
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndName 返回最远到达的列名
|
||||||
|
func (c Column) EndName() string {
|
||||||
|
return c.endName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name 当前列名
|
||||||
|
func (c Column) Name() string {
|
||||||
|
return c.current
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameWithRow 带行号的列名,比如:A1
|
||||||
|
func (c Column) NameWithRow(row int) string {
|
||||||
|
return fmt.Sprintf("%s%d", c.current, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset 重置到最开始的列(NewColumn 创建时的列)
|
||||||
|
func (c *Column) Reset() *Column {
|
||||||
|
c.current = c.startName
|
||||||
|
c.endName = c.startName
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// To 跳转到指定的列
|
||||||
|
func (c *Column) To(name string) (*Column, error) {
|
||||||
|
name = strings.ToUpper(name)
|
||||||
|
if !isValidName(name) {
|
||||||
|
return c, fmt.Errorf("invalid column name %s", name)
|
||||||
|
}
|
||||||
|
c.current = name
|
||||||
|
c.setEndName(name)
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Column) RightShift(steps int) (*Column, error) {
|
||||||
|
if steps <= 0 {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
return c.shift(steps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Column) LeftShift(steps int) (*Column, error) {
|
||||||
|
if steps <= 0 {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
return c.shift(-steps)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RightShift 基于当前位置右移多少列
|
||||||
|
func (c *Column) shift(steps int) (*Column, error) {
|
||||||
|
if steps == 0 {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
number := toNumber(c.current)
|
||||||
|
number += steps
|
||||||
|
if number > maxNumber {
|
||||||
|
return c, errors.New("out of max columns")
|
||||||
|
} else if number < minNumber {
|
||||||
|
return c, errors.New("out of min columns")
|
||||||
|
}
|
||||||
|
|
||||||
|
sb := strings.Builder{}
|
||||||
|
sb.Grow(3) // Max 3 letters
|
||||||
|
times := 0
|
||||||
|
for {
|
||||||
|
times++
|
||||||
|
quotient := number / 26
|
||||||
|
remainder := number % 26
|
||||||
|
if remainder == 0 {
|
||||||
|
sb.WriteRune('Z')
|
||||||
|
} else {
|
||||||
|
sb.WriteRune(rune(a + remainder))
|
||||||
|
}
|
||||||
|
if quotient == 0 {
|
||||||
|
break
|
||||||
|
} else if quotient <= 26 {
|
||||||
|
if quotient != 1 || (times >= 1 && remainder != 0) {
|
||||||
|
sb.WriteRune(rune(a + quotient))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
number = quotient
|
||||||
|
}
|
||||||
|
|
||||||
|
c.current = string(reverse(sb.String()))
|
||||||
|
if steps > 0 {
|
||||||
|
// Is right shift
|
||||||
|
c.setEndName(c.current)
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
88
spreedsheetx/column_test.go
Normal file
88
spreedsheetx/column_test.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package spreedsheetx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewColumn(t *testing.T) {
|
||||||
|
column := NewColumn("A")
|
||||||
|
assert.Equal(t, "A", column.Name())
|
||||||
|
column.Next()
|
||||||
|
assert.Equal(t, "B", column.Name())
|
||||||
|
column.RightShift(2)
|
||||||
|
assert.Equal(t, "D", column.Name())
|
||||||
|
column.To("F")
|
||||||
|
assert.Equal(t, "F", column.Name())
|
||||||
|
column.Next()
|
||||||
|
assert.Equal(t, "G", column.Name())
|
||||||
|
_, err := column.To("ZZZ")
|
||||||
|
assert.Equal(t, true, err != nil, "err")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewColumn2(t *testing.T) {
|
||||||
|
column := NewColumn("A")
|
||||||
|
column.To("ZZ")
|
||||||
|
assert.Equal(t, "ZZ", column.Name())
|
||||||
|
column.RightShift(2)
|
||||||
|
assert.Equal(t, "AAB", column.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewColumn3(t *testing.T) {
|
||||||
|
column := NewColumn("ABC")
|
||||||
|
assert.Equal(t, "ABC", column.Name())
|
||||||
|
column.RightShift(2)
|
||||||
|
assert.Equal(t, "ABE", column.Name())
|
||||||
|
column.RightShift(53)
|
||||||
|
assert.Equal(t, "ADF", column.Name())
|
||||||
|
column.To("A")
|
||||||
|
column.Next()
|
||||||
|
assert.Equal(t, "B", column.Name())
|
||||||
|
column.Next()
|
||||||
|
assert.Equal(t, "C", column.Name())
|
||||||
|
column.To("A")
|
||||||
|
column.RightShift(26)
|
||||||
|
assert.Equal(t, "AA", column.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewColumn4(t *testing.T) {
|
||||||
|
column := NewColumn("A")
|
||||||
|
column.RightShift(1000)
|
||||||
|
assert.Equal(t, "ALM", column.Name())
|
||||||
|
column.To("A")
|
||||||
|
column.RightShift(25)
|
||||||
|
assert.Equal(t, "Z", column.Name())
|
||||||
|
column.RightShift(1)
|
||||||
|
assert.Equal(t, "AA", column.Name())
|
||||||
|
column.To("A")
|
||||||
|
column.RightShift(maxNumber - 1)
|
||||||
|
assert.Equal(t, "XFD", column.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewColumn5(t *testing.T) {
|
||||||
|
column := NewColumn("A")
|
||||||
|
column.Next()
|
||||||
|
column.RightShift(26)
|
||||||
|
assert.Equal(t, "AB", column.Name())
|
||||||
|
column.Reset()
|
||||||
|
assert.Equal(t, "A", column.Name())
|
||||||
|
assert.Equal(t, "A", column.StartName())
|
||||||
|
assert.Equal(t, "A", column.EndName())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewLeftShift(t *testing.T) {
|
||||||
|
column := NewColumn("Z")
|
||||||
|
column.Prev()
|
||||||
|
assert.Equal(t, "Y", column.Name())
|
||||||
|
column.LeftShift(23)
|
||||||
|
assert.Equal(t, "B", column.Name())
|
||||||
|
column.LeftShift(1)
|
||||||
|
assert.Equal(t, "A", column.Name())
|
||||||
|
_, err := column.LeftShift(1)
|
||||||
|
assert.Equal(t, err != nil, true, "err")
|
||||||
|
column.Reset()
|
||||||
|
assert.Equal(t, "Z", column.Name())
|
||||||
|
assert.Equal(t, "Z", column.StartName())
|
||||||
|
column.To("AA")
|
||||||
|
assert.Equal(t, "AA", column.EndName())
|
||||||
|
}
|
||||||
554
stringx/string.go
Normal file
554
stringx/string.go
Normal file
File diff suppressed because one or more lines are too long
547
stringx/string_test.go
Normal file
547
stringx/string_test.go
Normal file
@@ -0,0 +1,547 @@
|
|||||||
|
package stringx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.cloudyne.io/go/hiscaler-gox/slicex"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsEmpty(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
String string
|
||||||
|
IsEmpty bool
|
||||||
|
}{
|
||||||
|
{"A", false},
|
||||||
|
{"", true},
|
||||||
|
{" ", false},
|
||||||
|
{" ", false},
|
||||||
|
{" ", false},
|
||||||
|
{`
|
||||||
|
|
||||||
|
|
||||||
|
`, false},
|
||||||
|
{`
|
||||||
|
|
||||||
|
a
|
||||||
|
|
||||||
|
`, false},
|
||||||
|
}
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
b := IsEmpty(testCase.String)
|
||||||
|
if b != testCase.IsEmpty {
|
||||||
|
t.Errorf("%d: %s except %v, actual %v", i, testCase.String, testCase.IsEmpty, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsBlank(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
String string
|
||||||
|
IsEmpty bool
|
||||||
|
}{
|
||||||
|
{"A", false},
|
||||||
|
{"", true},
|
||||||
|
{" ", true},
|
||||||
|
{" ", true},
|
||||||
|
{" ", true},
|
||||||
|
{`
|
||||||
|
|
||||||
|
|
||||||
|
`, true},
|
||||||
|
{`
|
||||||
|
|
||||||
|
a
|
||||||
|
|
||||||
|
`, false},
|
||||||
|
}
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
b := IsBlank(testCase.String)
|
||||||
|
if b != testCase.IsEmpty {
|
||||||
|
t.Errorf("%d: %s except %v, actual %v", i, testCase.String, testCase.IsEmpty, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainsChinese(t *testing.T) {
|
||||||
|
type testCast struct {
|
||||||
|
String string
|
||||||
|
Has bool
|
||||||
|
}
|
||||||
|
testCasts := []testCast{
|
||||||
|
{"", false},
|
||||||
|
{"a", false},
|
||||||
|
{"A_B", false},
|
||||||
|
{"A_中B", true},
|
||||||
|
}
|
||||||
|
for _, tc := range testCasts {
|
||||||
|
has := ContainsChinese(tc.String)
|
||||||
|
if has != tc.Has {
|
||||||
|
t.Errorf("%s except %v, actual:%v", tc.String, tc.Has, has)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToNarrow(t *testing.T) {
|
||||||
|
testCasts := []struct {
|
||||||
|
tag string
|
||||||
|
string string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t-letter", "abc", "abc"},
|
||||||
|
{"t-number", "0123456789", "0123456789"},
|
||||||
|
{"t-letter-number", "a0", "a0"},
|
||||||
|
{"t-other", "~!@#$%^&*()-+?", "~!@#$%^&*()-+?"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCasts {
|
||||||
|
value := ToNarrow(tc.string)
|
||||||
|
assert.Equal(t, tc.expected, value, tc.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToWiden(t *testing.T) {
|
||||||
|
testCasts := []struct {
|
||||||
|
tag string
|
||||||
|
string string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t-letter", "abc", "abc"},
|
||||||
|
{"t-number", "0123456789", "0123456789"},
|
||||||
|
{"t-letter-number", "a0", "a0"},
|
||||||
|
{"t-other", "~!@#$%^&*()-+?", "~!@#$%^&*()-+?"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCasts {
|
||||||
|
value := ToWiden(tc.string)
|
||||||
|
assert.Equal(t, tc.expected, value, tc.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplit(t *testing.T) {
|
||||||
|
type testCast struct {
|
||||||
|
Number int
|
||||||
|
String string
|
||||||
|
Seps []string
|
||||||
|
Values []string
|
||||||
|
}
|
||||||
|
testCasts := []testCast{
|
||||||
|
{1, "abc", []string{}, []string{"abc"}},
|
||||||
|
{2, "a b c", []string{}, []string{"a b c"}},
|
||||||
|
{3, "a b c,d", []string{}, []string{"a b c,d"}},
|
||||||
|
{4, "a b c,d", []string{",", " "}, []string{"a", "b", "c", "d"}},
|
||||||
|
{5, "a,b,c,d", []string{",", " "}, []string{"a", "b", "c", "d"}},
|
||||||
|
{6, "a,b,c,d e", []string{",", " "}, []string{"a", "b", "c", "d", "e"}},
|
||||||
|
{7, "a.,b,c,d e", []string{",", " "}, []string{"a.", "b", "c", "d", "e"}},
|
||||||
|
{8, "a.,b,c,d e", []string{",", ".", " "}, []string{"a", "b", "c", "d", "e"}},
|
||||||
|
{9, "a.,b,c,d e", []string{",", " "}, []string{"a.", "b", "c", "d", "e"}},
|
||||||
|
{10, "a.,b,c,d e", []string{",", "", " "}, []string{"a", ".", "b", "c", "d", "e"}},
|
||||||
|
{11, "hello, world!!!", []string{",", " ", "!"}, []string{"hello", "world"}},
|
||||||
|
{12, "WaterWipes Original Baby Wipes, 99.9% Water, Unscented & Hypoallergenic for Sensitive Newborn Skin, 3 Packs (180 Count)", []string{",", " ", "!"}, []string{"WaterWipes", "Original", "Baby", "Wipes", "99.9%", "Water", "Unscented", "&", "Hypoallergenic", "for", "Sensitive", "Newborn", "Skin", "3", "Packs", "(180", "Count)"}},
|
||||||
|
}
|
||||||
|
for _, tc := range testCasts {
|
||||||
|
values := Split(tc.String, tc.Seps...)
|
||||||
|
if !slicex.StringSliceEqual(values, tc.Values, false, false, true) {
|
||||||
|
t.Errorf("%d except %#v, actual:%#v", tc.Number, tc.Values, values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestString(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
Number int
|
||||||
|
Value interface{}
|
||||||
|
Except string
|
||||||
|
}{
|
||||||
|
{1, false, "false"},
|
||||||
|
{2, true, "true"},
|
||||||
|
{3, 1, "1"},
|
||||||
|
{4, 1.1, "1.1"},
|
||||||
|
{5, "abc", "abc"},
|
||||||
|
{6, [2]int{1, 2}, "[1,2]"},
|
||||||
|
{7, []int{1, 2}, "[1,2]"},
|
||||||
|
{8, []string{"a", "b"}, `["a","b"]`},
|
||||||
|
{9, struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
}{ID: 1, Name: "John"}, `{"ID":1,"Name":"John"}`},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
s := String(testCase.Value)
|
||||||
|
if !strings.EqualFold(s, testCase.Except) {
|
||||||
|
t.Errorf("%d except: %s, actual: %s", testCase.Number, testCase.Except, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveEmoji(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
Number int
|
||||||
|
BeforeString string
|
||||||
|
AfterString string
|
||||||
|
}{
|
||||||
|
{1, "👶hi", "hi"},
|
||||||
|
{2, "1👰", "1"},
|
||||||
|
{3, "1👉2🤟👉👰3🤟👉👶你好🤟", "123你好"},
|
||||||
|
{4, "1👉2🤟👉👰3🤟👉👶你 好🤟", "123你 好"},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
s := RemoveEmoji(testCase.BeforeString, true)
|
||||||
|
if !strings.EqualFold(s, testCase.AfterString) {
|
||||||
|
t.Errorf("%d except: %s, actual: %s", testCase.Number, testCase.AfterString, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrimAny(t *testing.T) {
|
||||||
|
var testCases = []struct {
|
||||||
|
string string
|
||||||
|
replacePairs []string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{" a", []string{}, " a"},
|
||||||
|
{" 10GGGGgggggg", []string{"", "G"}, "10"},
|
||||||
|
{" A", []string{}, " A"},
|
||||||
|
{" Abc", []string{""}, "Abc"},
|
||||||
|
{" Abc", []string{"", "", " "}, "Abc"},
|
||||||
|
{" Abcd Efg ", []string{"", "ab", "FG"}, "cd E"},
|
||||||
|
{" Abcd中文 Efg ", []string{"", "abcd", "中", "FG"}, "文 E"},
|
||||||
|
{" Abcd中文 Efg ", []string{"", "中", "abcd", "FG"}, "文 E"},
|
||||||
|
{" a", []string{"b", "c"}, " a"},
|
||||||
|
{" 10kg", []string{"g", "kg", ""}, "10"},
|
||||||
|
{" 10kgg", []string{"g", "kg", ""}, "10"},
|
||||||
|
{" 10kg g", []string{"g", "kg", ""}, "10"},
|
||||||
|
{" 10kg agbg", []string{"g", "ag", "bg", "kg", ""}, "10"},
|
||||||
|
{" 10kg abgcdg", []string{"bg", "abg", "cdg", "kg", ""}, "10"},
|
||||||
|
{" 10kg abgcdg", []string{"a", "b", "c", "d", "g", ""}, "10k"},
|
||||||
|
{" 10kg ggkgg", []string{"kg", "g", ""}, "10"},
|
||||||
|
{" a", []string{"a", "c"}, " "},
|
||||||
|
{" a ", []string{}, " a "},
|
||||||
|
{`
|
||||||
|
|
||||||
|
a
|
||||||
|
|
||||||
|
`, []string{}, `
|
||||||
|
|
||||||
|
a
|
||||||
|
|
||||||
|
`},
|
||||||
|
{" ab", []string{"b"}, " a"},
|
||||||
|
{" a b ", []string{"b"}, " a b "},
|
||||||
|
{" a b b", []string{"b"}, " a b "},
|
||||||
|
{" a b a", []string{"b"}, " a b a"},
|
||||||
|
{"5.0 out of 5 stars", []string{"5.0 out of", "stars"}, " 5 "},
|
||||||
|
{"5.0 out of 5 stars", []string{"5.0 out of", "stars", ""}, "5"},
|
||||||
|
{"5.0 out of 5 stars", []string{"5.0 out of", "5", "stars", " "}, ""},
|
||||||
|
{"a b a b c d e f g g f e d", []string{"a", "b", "c", "d", "f g", " "}, "e f g g f e"},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
actual := TrimAny(testCase.string, testCase.replacePairs...)
|
||||||
|
if actual != testCase.expected {
|
||||||
|
t.Errorf("TrimAny(`%s`, %#v) = `%s`; expected `%s`", testCase.string, testCase.replacePairs, actual, testCase.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkTrimAny(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
TrimAny("a b a b c d e f g g f e d", "a", "b", "c", "d", "f g", " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveExtraSpace(t *testing.T) {
|
||||||
|
var testCases = []struct {
|
||||||
|
number int
|
||||||
|
string string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{1, " a", "a"},
|
||||||
|
{2, " a", "a"},
|
||||||
|
{3, " a", "a"},
|
||||||
|
{4, " a ", "a"},
|
||||||
|
{5, `
|
||||||
|
|
||||||
|
a
|
||||||
|
|
||||||
|
`, "a"},
|
||||||
|
{6, " ab", "ab"},
|
||||||
|
{7, " a b ", "a b"},
|
||||||
|
{8, " a b b", "a b b"},
|
||||||
|
{9, " hello, world!", "hello, world!"},
|
||||||
|
{10, `
|
||||||
|
hello,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
world!
|
||||||
|
`, "hello, world!"},
|
||||||
|
{11, `
|
||||||
|
|
||||||
|
<div a="1" b="2">
|
||||||
|
|
||||||
|
<span>
|
||||||
|
|
||||||
|
hello world
|
||||||
|
|
||||||
|
</span>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
`, `<div a="1" b="2"> <span> hello world </span> </div>`},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
actual := RemoveExtraSpace(testCase.string)
|
||||||
|
if actual != testCase.expected {
|
||||||
|
t.Errorf("%d RemoveExtraSpace(%s) = '%s'; expected %s", testCase.number, testCase.string, actual, testCase.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToBytes(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
bytesValue []byte
|
||||||
|
string string
|
||||||
|
}{
|
||||||
|
{"t1", []byte{'a'}, "a"},
|
||||||
|
{"t2", []byte("abc"), "abc"},
|
||||||
|
{"t3", []byte("a b c "), "a b c "},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
b := ToBytes(test.string)
|
||||||
|
assert.Equal(t, test.bytesValue, b, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWordMatched(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
string string
|
||||||
|
words []string
|
||||||
|
caseSensitive bool
|
||||||
|
except bool
|
||||||
|
}{
|
||||||
|
{"t1", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{"Towels", "B"}, true, true},
|
||||||
|
{"t2", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{"towels", "B"}, false, true},
|
||||||
|
{"t3.1", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{"tow", "A", "B"}, false, true},
|
||||||
|
{"t3.2", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{"tow", "A", "B"}, true, false},
|
||||||
|
{"t4", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{"tow"}, false, false},
|
||||||
|
{"t5.1", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{"20"}, false, false},
|
||||||
|
{"t5.2", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{"200"}, false, true},
|
||||||
|
{"t6", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{"Blue Shop"}, true, true},
|
||||||
|
{"t7.1", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{"blue shop"}, false, true},
|
||||||
|
{"t7.2", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{"blue shop"}, true, false},
|
||||||
|
{"t8", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{"Scott "}, false, true},
|
||||||
|
{"t9", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{"Sheets "}, false, true},
|
||||||
|
{"t10.1", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{`.`}, false, false},
|
||||||
|
{"t10.2", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{`...................`}, false, false},
|
||||||
|
{"t11", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{"*"}, false, false},
|
||||||
|
{"t12", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{"***"}, false, false},
|
||||||
|
{"t13.1", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{`.*`}, false, false},
|
||||||
|
{"t13.2", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{`B.*x`}, false, false},
|
||||||
|
{"t14", "Scott Blue Shop Towels in a Box - 200 Sheets.", []string{"."}, false, false},
|
||||||
|
{"t14.1", "Scott Blue Shop Towels in a Box - 200 Sheets.", []string{".*"}, false, false},
|
||||||
|
{"t14.2", "Scott Blue Shop Towels in a Box - 200 Sheets.", []string{"Sheets."}, false, true},
|
||||||
|
{"t15", "Scott Blue Shop Towels in a Box - 200 Sheets?", []string{"?"}, false, false},
|
||||||
|
{"t16", "Scott Blue Shop Towels in a Box - 200 Sheets", []string{"[Sheets]"}, false, false},
|
||||||
|
{"t17", "Scott Blue Shop Towels in a Box a-a 200 Sheets", []string{"a-a"}, false, true},
|
||||||
|
{"t18", "Scott Blue Shop Towels in a Box--200 Sheets", []string{"-"}, false, false},
|
||||||
|
{"t19", "Scott Blue Shop Towels in a Box--200 Sheets", []string{"--"}, false, false},
|
||||||
|
{"t20", "Scott Blue Shop Towels in a Box--200 Sheets", []string{"Box--200"}, false, true},
|
||||||
|
{"t20.1", "Scott Blue Shop Towels in a Box~200 Sheets", []string{"Box"}, false, false},
|
||||||
|
{"t21", "Scott Blue Shop Towels in a Box--200 Sheets 中文", []string{"中文"}, false, true},
|
||||||
|
{"t22", "中文汉字", []string{"汉字"}, false, true},
|
||||||
|
{"t23", "中a文b汉c字", []string{"汉c字"}, false, true},
|
||||||
|
{"t24", "中a文b汉c字", []string{"汉C字"}, false, true},
|
||||||
|
{"t25", "中a文b汉c字", []string{"汉C字"}, true, false},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
b := WordMatched(test.string, test.words, test.caseSensitive)
|
||||||
|
assert.Equal(t, test.except, b, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkWordMatched(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
WordMatched("Scott Blue Shop Towels in a Box--200 Sheets", []string{"Throw Pillow Covers", "Throw Pillows", "Patio Furniture Pillows", "Pillow Covers", "Pillowcases", "Pillow Case", "Pillow Cover", "scot", "scottt", "blu", "Shop Towels"}, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartsWith(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
string string
|
||||||
|
words []string
|
||||||
|
caseSensitive bool
|
||||||
|
except bool
|
||||||
|
}{
|
||||||
|
{"t1", "Hello world!", []string{"he", "He"}, false, true},
|
||||||
|
{"t2", "Hello world!", []string{"he", "He"}, true, true},
|
||||||
|
{"t3", "Hello world!", []string{"he"}, true, false},
|
||||||
|
{"t4", "", []string{""}, true, true},
|
||||||
|
{"t5", "", nil, true, true},
|
||||||
|
{"t6", "", []string{}, true, true},
|
||||||
|
{"t7", "Hello world!", []string{""}, true, true},
|
||||||
|
{"t8", "Hello!", []string{"Hello world"}, true, false},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
b := StartsWith(test.string, test.words, test.caseSensitive)
|
||||||
|
assert.Equal(t, test.except, b, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkStartsWith(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
StartsWith("Hello world!", []string{"a", "b", "c", "d", "e", "f", "g", "h"}, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEndsWith(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
string string
|
||||||
|
words []string
|
||||||
|
caseSensitive bool
|
||||||
|
except bool
|
||||||
|
}{
|
||||||
|
{"t1", "Hello world!", []string{"he", "He"}, false, false},
|
||||||
|
{"t2", "Hello world!", []string{"he", "He"}, true, false},
|
||||||
|
{"t3", "Hello world!", []string{"d!", "!"}, true, true},
|
||||||
|
{"t4", "Hello world!", []string{"WORLD!"}, false, true},
|
||||||
|
{"t5", "", []string{""}, true, true},
|
||||||
|
{"t6", "", nil, true, true},
|
||||||
|
{"t7", "", []string{}, true, true},
|
||||||
|
{"t8", "Hello world!", []string{""}, true, true},
|
||||||
|
{"t9", "world!", []string{"hello world!", "world!", "!"}, true, true},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
b := EndsWith(test.string, test.words, test.caseSensitive)
|
||||||
|
assert.Equal(t, test.except, b, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContains(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
string string
|
||||||
|
words []string
|
||||||
|
caseSensitive bool
|
||||||
|
except bool
|
||||||
|
}{
|
||||||
|
{"t1", "Hello world!", []string{"ol", "LL"}, false, true},
|
||||||
|
{"t2", "Hello world!", []string{"ol", "LL"}, true, false},
|
||||||
|
{"t3", "Hello world!", []string{"notfound", "world"}, false, true},
|
||||||
|
{"t4", "Hello world!", []string{"notfound", "world"}, true, true},
|
||||||
|
{"t5", "", []string{""}, true, true},
|
||||||
|
{"t6", "Hello world!", []string{""}, true, true},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
b := Contains(test.string, test.words, test.caseSensitive)
|
||||||
|
assert.Equal(t, test.except, b, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkContains(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
Contains("Customer satisfaction is important to us. We are confident with our fuzzy blanket, but if you are not satisfied with our blanket feel free to contact us. we will provide you with the most satisfactory solution", []string{"free"}, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuoteMeta(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
string string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t1", `.+\()[]$^*?`, `\.\+\\\(\)\[\]\$\^\*\?`},
|
||||||
|
{"t1", `.+\()[]$^*?{}`, `\.\+\\\(\)\[\]\$\^\*\?{}`},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
b := QuoteMeta(test.string)
|
||||||
|
assert.Equal(t, test.expected, b, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSequentialWordFields(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
tag string
|
||||||
|
string string
|
||||||
|
n int
|
||||||
|
separators []string
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{"t1", "hello world", 1, []string{}, []string{"hello", "world"}},
|
||||||
|
{"t2", "hello world", 2, []string{}, []string{"hello", "world", "hello world"}},
|
||||||
|
{"t3", "hello world", 2, []string{}, []string{"hello", "world", "hello world"}},
|
||||||
|
{"t4", "this is a string", 1, []string{}, []string{"this", "is", "a", "string"}},
|
||||||
|
{"t5", "this is a string", 2, []string{}, []string{"this", "is", "a", "string", "this is", "is a", "a string"}},
|
||||||
|
{"t6", "this is a string", 3, []string{}, []string{"this", "is", "a", "string", "this is", "this is a", "is a", "is a string", "a string"}},
|
||||||
|
{"t7", "What's you name? My name is XiaoMing.", 3, []string{"?"}, []string{"What's", "you", "name", "My", "is", "XiaoMing", "What's you", "What's you name", "you name", "My name", "My name is", "name is", "name is XiaoMing", "is XiaoMing"}},
|
||||||
|
{"t8", "a1, a2? b1 2b?", 3, []string{","}, []string{"a1", "a2", "b1", "2b", "a2 b1", "a2 b1 2b", "b1 2b"}},
|
||||||
|
{"t9", "a1, a?2? b1 2b?~", 3, []string{","}, []string{"a1", "a?2", "b1", "2b", "a?2 b1", "a?2 b1 2b", "b1 2b"}},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
v := SequentialWordFields(test.string, test.n, test.separators...)
|
||||||
|
assert.ElementsMatch(t, test.expected, v, test.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSequentialWordFields(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
SequentialWordFields("What's you name? My name is XiaoMing.", 3, []string{"?"}...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLen(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
string string
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{"t1", "hello", 5},
|
||||||
|
{"t1", "hello world", 11},
|
||||||
|
{"t1", "hello中国", 7},
|
||||||
|
{"t1", "hello 中国", 8},
|
||||||
|
{"t1", "你好中国", 4},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
n := Len(testCase.string)
|
||||||
|
assert.Equal(t, testCase.expected, n, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpperFirst(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
string string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t1", "hello", "Hello"},
|
||||||
|
{"t1", "hello world", "Hello world"},
|
||||||
|
{"t1", "hello中国", "Hello中国"},
|
||||||
|
{"t1", "hello 中国", "Hello 中国"},
|
||||||
|
{"t1", "你好中国", "你好中国"},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
s := UpperFirst(testCase.string)
|
||||||
|
assert.Equal(t, testCase.expected, s, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLowerFirst(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
string string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t1", "Hello", "hello"},
|
||||||
|
{"t1", "Hello world", "hello world"},
|
||||||
|
{"t1", "Hello中国", "hello中国"},
|
||||||
|
{"t1", "Hello 中国", "hello 中国"},
|
||||||
|
{"t1", "你好中国", "你好中国"},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
s := LowerFirst(testCase.string)
|
||||||
|
assert.Equal(t, testCase.expected, s, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
123
timex/time.go
Normal file
123
timex/time.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package timex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsAmericaSummerTime 是否为美国夏令时间
|
||||||
|
// 夏令时开始于每年3月的第二个周日凌晨,人们需要将时间调早 (顺时针) 1个小时;
|
||||||
|
// 夏令时结束于每年11月的第一个周日凌晨,人们需要将时间调晚 (逆时针) 1个小时。
|
||||||
|
func IsAmericaSummerTime(t time.Time) (yes bool) {
|
||||||
|
if t.IsZero() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
month := t.Month()
|
||||||
|
switch month {
|
||||||
|
case 4, 5, 6, 7, 8, 9, 10:
|
||||||
|
yes = true
|
||||||
|
case 3, 11:
|
||||||
|
day := t.Day()
|
||||||
|
t1 := t.AddDate(0, 0, -day+1)
|
||||||
|
weekday := int(t1.Weekday())
|
||||||
|
if (month == 3 && day >= t1.AddDate(0, 0, 14-weekday).Day()) ||
|
||||||
|
(month == 11 && day < t1.AddDate(0, 0, 7-weekday).Day()) {
|
||||||
|
yes = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChineseTimeLocation Return chinese time location
|
||||||
|
func ChineseTimeLocation() *time.Location {
|
||||||
|
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||||
|
if err != nil {
|
||||||
|
loc = time.FixedZone("CST", 8*3600)
|
||||||
|
}
|
||||||
|
return loc
|
||||||
|
}
|
||||||
|
|
||||||
|
func Between(t, begin, end time.Time) bool {
|
||||||
|
return (t.After(begin) && t.Before(end)) || t.Equal(begin) || t.Equal(end)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DayStart(t time.Time) time.Time {
|
||||||
|
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.Local)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DayEnd(t time.Time) time.Time {
|
||||||
|
return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 999999999, time.Local)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MonthStart(t time.Time) time.Time {
|
||||||
|
return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, time.Local)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MonthEnd(t time.Time) time.Time {
|
||||||
|
return DayEnd(MonthStart(t).AddDate(0, 1, -1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAM Check is AM
|
||||||
|
func IsAM(t time.Time) bool {
|
||||||
|
return t.Hour() <= 11
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPM Check is PM
|
||||||
|
func IsPM(t time.Time) bool {
|
||||||
|
return t.Hour() >= 12
|
||||||
|
}
|
||||||
|
|
||||||
|
func WeekStart(yearWeek int) time.Time {
|
||||||
|
year := yearWeek / 100
|
||||||
|
week := yearWeek % year
|
||||||
|
// Start from the middle of the year:
|
||||||
|
t := time.Date(year, 7, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
// Roll back to Monday:
|
||||||
|
if wd := t.Weekday(); wd == time.Sunday {
|
||||||
|
t = t.AddDate(0, 0, -6)
|
||||||
|
} else {
|
||||||
|
t = t.AddDate(0, 0, -int(wd)+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Difference in weeks:
|
||||||
|
_, w := t.ISOWeek()
|
||||||
|
t = t.AddDate(0, 0, (week-w)*7)
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func WeekEnd(yearWeek int) time.Time {
|
||||||
|
t := WeekStart(yearWeek).AddDate(0, 0, 6)
|
||||||
|
return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, time.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func YearWeeksByWeek(startYearWeek, endYearWeek int) []int {
|
||||||
|
weeks := make([]int, 0)
|
||||||
|
weekStart := WeekStart(startYearWeek)
|
||||||
|
weekEnd := WeekStart(endYearWeek)
|
||||||
|
for {
|
||||||
|
if weekStart.After(weekEnd) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
y, w := weekStart.ISOWeek()
|
||||||
|
weeks = append(weeks, y*100+w)
|
||||||
|
weekStart = weekStart.AddDate(0, 0, 7)
|
||||||
|
}
|
||||||
|
return weeks
|
||||||
|
}
|
||||||
|
|
||||||
|
func YearWeeksByTime(startDate, endDate time.Time) []int {
|
||||||
|
y1, w1 := startDate.ISOWeek()
|
||||||
|
y2, w2 := endDate.ISOWeek()
|
||||||
|
return YearWeeksByWeek(y1*100+w1, y2*100+w2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// XISOWeek 非 ISO 周,从周日开始算起作为一周的第一天
|
||||||
|
func XISOWeek(t time.Time) (year, week int) {
|
||||||
|
t1 := time.Date(t.Year(), t.Month(), t.Day()+4-int(t.Weekday()), 0, 0, 0, 0, time.UTC)
|
||||||
|
startTime := time.Date(t1.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
week = int(math.Ceil((float64((t1.Unix()-startTime.Unix())/86400) + 1) / 7))
|
||||||
|
return startTime.Year(), week
|
||||||
|
}
|
||||||
217
timex/time_test.go
Normal file
217
timex/time_test.go
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
package timex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsAmericaSummerTime(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
Date string
|
||||||
|
SummerTime bool
|
||||||
|
}{
|
||||||
|
{"0001-01-01", false},
|
||||||
|
{"2021-11-10", false},
|
||||||
|
{"2021-12-10", false},
|
||||||
|
{"2021-03-10", false},
|
||||||
|
{"2021-03-14", true},
|
||||||
|
{"2021-11-01", true},
|
||||||
|
{"2021-10-10", true},
|
||||||
|
{"2021-10-11", true},
|
||||||
|
{"2021-10-12", true},
|
||||||
|
{"2021-12-12", false},
|
||||||
|
{"2022-03-15", true},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
d, _ := time.Parse("2006-01-02", testCase.Date)
|
||||||
|
v := IsAmericaSummerTime(d)
|
||||||
|
if v != testCase.SummerTime {
|
||||||
|
t.Errorf("%s except %v, actual %v", testCase.Date, testCase.SummerTime, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBetween(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
t string
|
||||||
|
begin string
|
||||||
|
end string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"t1", "2022-01-01", "2022-01-01", "2022-01-01", true},
|
||||||
|
{"t2", "2022-01-02", "2022-01-01", "2022-01-01", false},
|
||||||
|
{"t3", "2022-01-02", "2022-01-01", "2022-01-02", true},
|
||||||
|
}
|
||||||
|
layout := "2006-01-02"
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
tv, _ := time.Parse(layout, testCase.t)
|
||||||
|
begin, _ := time.Parse(layout, testCase.begin)
|
||||||
|
end, _ := time.Parse(layout, testCase.end)
|
||||||
|
v := Between(tv, begin, end)
|
||||||
|
assert.Equal(t, testCase.expected, v, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDayStart(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
time string
|
||||||
|
layout string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t1", "2022-01-01T12:12:00.924Z", "2006-01-02T15:04:05Z", "2022-01-01 00:00:00"},
|
||||||
|
{"t2", "2022-01-01 00:00:00", "2006-01-02 15:04:05", "2022-01-01 00:00:00"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
tv, err := time.Parse(testCase.layout, testCase.time)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
}
|
||||||
|
v := DayStart(tv).Format("2006-01-02 15:04:05")
|
||||||
|
assert.Equal(t, testCase.expected, v, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDayEnd(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
time string
|
||||||
|
layout string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t1", "2022-01-01 12:12:00", "2006-01-02 15:04:05", "2022-01-01 23:59:59"},
|
||||||
|
{"t2", "2022-01-01 00:00:00", "2006-01-02 15:04:05", "2022-01-01 23:59:59"},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
tv, _ := time.Parse(testCase.layout, testCase.time)
|
||||||
|
v := DayEnd(tv).Format("2006-01-02 15:04:05")
|
||||||
|
assert.Equal(t, testCase.expected, v, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMonthStart(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
time string
|
||||||
|
layout string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t1", "2022-01-12 12:12:00", "2006-01-02 15:04:05", "2022-01-01 00:00:00"},
|
||||||
|
{"t2", "2022-01-21 00:00:00", "2006-01-02 15:04:05", "2022-01-01 00:00:00"},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
tv, _ := time.Parse(testCase.layout, testCase.time)
|
||||||
|
v := MonthStart(tv).Format("2006-01-02 15:04:05")
|
||||||
|
assert.Equal(t, testCase.expected, v, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMonthEnd(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
time string
|
||||||
|
layout string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t1", "2022-01-12 12:12:00", "2006-01-02 15:04:05", "2022-01-31 23:59:59"},
|
||||||
|
{"t2", "2022-02-21 00:00:00", "2006-01-02 15:04:05", "2022-02-28 23:59:59"},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
tv, _ := time.Parse(testCase.layout, testCase.time)
|
||||||
|
v := MonthEnd(tv).Format("2006-01-02 15:04:05")
|
||||||
|
assert.Equal(t, testCase.expected, v, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWeekStart(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
yearWeek int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t1", 202201, "2022-01-03 00:00:00"},
|
||||||
|
{"t2", 202202, "2022-01-10 00:00:00"},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
v := WeekStart(testCase.yearWeek).Format("2006-01-02 15:04:05")
|
||||||
|
assert.Equal(t, testCase.expected, v, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWeekEnd(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
yearWeek int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t1", 202201, "2022-01-09 23:59:59"},
|
||||||
|
{"t2", 202202, "2022-01-16 23:59:59"},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
v := WeekEnd(testCase.yearWeek).Format("2006-01-02 15:04:05")
|
||||||
|
assert.Equal(t, testCase.expected, v, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestYearWeeksByWeek(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
beginYearWeek int
|
||||||
|
endYearWeek int
|
||||||
|
expected []int
|
||||||
|
}{
|
||||||
|
{"t1", 202201, 202202, []int{202201, 202202}},
|
||||||
|
{"t2", 202201, 202204, []int{202201, 202202, 202203, 202204}},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
v := YearWeeksByWeek(testCase.beginYearWeek, testCase.endYearWeek)
|
||||||
|
assert.Equal(t, testCase.expected, v, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestYearWeeksByTime(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
beginDate string
|
||||||
|
endDate string
|
||||||
|
expected []int
|
||||||
|
}{
|
||||||
|
{"t1", "2022-01-01", "2022-01-02", []int{202152}},
|
||||||
|
{"t2", "2022-01-01", "2022-02-02", []int{202152, 202201, 202202, 202203, 202204, 202205}},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
beginDate, _ := time.Parse("2006-01-02", testCase.beginDate)
|
||||||
|
endDate, _ := time.Parse("2006-01-02", testCase.endDate)
|
||||||
|
v := YearWeeksByTime(beginDate, endDate)
|
||||||
|
assert.Equal(t, testCase.expected, v, testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://savvytime.com/week-number
|
||||||
|
func TestXISOWeek(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tag string
|
||||||
|
date string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"t1", "2022-01-01", "202152"},
|
||||||
|
{"t2", "2022-01-02", "202201"},
|
||||||
|
{"t3", "2022-01-09", "202202"},
|
||||||
|
{"t4", "2022-01-10", "202202"},
|
||||||
|
{"t5", "2022-01-15", "202202"},
|
||||||
|
{"t6", "2022-01-16", "202203"},
|
||||||
|
{"t7", "2022-01-17", "202203"},
|
||||||
|
{"t8", "2022-01-29", "202204"},
|
||||||
|
{"t9", "2022-12-25", "202252"},
|
||||||
|
{"t10", "2023-01-01", "202301"},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
d, _ := time.Parse("2006-01-02", testCase.date)
|
||||||
|
year, week := XISOWeek(d)
|
||||||
|
assert.Equal(t, testCase.expected, fmt.Sprintf("%d%02d", year, week), testCase.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user