From d13de8f3fa18e632bac7c899ff97571b7c745b6b Mon Sep 17 00:00:00 2001 From: 4566704 <4566704@qq.com> Date: Sun, 20 Sep 2026 12:20:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(stringx):=20=E8=87=AA=20go-hua/common=20?= =?UTF-8?q?=E8=BF=81=E5=85=A5=E5=AD=97=E7=AC=A6=E4=B8=B2/=E5=88=87?= =?UTF-8?q?=E7=89=87/=E6=8B=BC=E9=9F=B3=E5=B7=A5=E5=85=B7=E5=B9=B6?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E6=88=90=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - common 的 string.go/array.go/pinyin.go 拆为 stringx 包(含随机串、GUID、 通配符匹配、校验规则、链式缓冲、驼峰转下划线、切片删元素、拼音首字母) - 附单元测试与 examples/stringx 可运行例程、包 README - go.mod 新增直接依赖 mozillazg/go-pinyin(go-hua 同版本锁 v0.20.0→实际解析 v0.21.0) --- examples/stringx/main.go | 33 +++++ go.mod | 3 +- go.sum | 2 + stringx/README.md | 51 ++++++++ stringx/array.go | 12 ++ stringx/pinyin.go | 26 ++++ stringx/stringx.go | 257 +++++++++++++++++++++++++++++++++++++++ stringx/stringx_test.go | 141 +++++++++++++++++++++ 8 files changed, 524 insertions(+), 1 deletion(-) create mode 100644 examples/stringx/main.go create mode 100644 stringx/README.md create mode 100644 stringx/array.go create mode 100644 stringx/pinyin.go create mode 100644 stringx/stringx.go create mode 100644 stringx/stringx_test.go diff --git a/examples/stringx/main.go b/examples/stringx/main.go new file mode 100644 index 0000000..4d22209 --- /dev/null +++ b/examples/stringx/main.go @@ -0,0 +1,33 @@ +// stringx 包示例:字符串/切片工具(随机串、GUID、取中间串、驼峰转下划线、 +// 通配符匹配、校验、中文首字母、切片删元素) +package main + +import ( + "fmt" + + "git.zeroonesoft.cn/golib/zogo/stringx" +) + +func main() { + // 随机字符串 / GUID + fmt.Println("随机字符串:", stringx.CreateRandomString(16)) + fmt.Println("纯数字随机:", stringx.CreateRandomStringInt(6)) + fmt.Println("GUID:", stringx.GetGUID()) + + // 字符串处理 + fmt.Println("取中间字符串:", stringx.GetBetweenStr("key=value&x=1", "key=", "&")) + fmt.Println("驼峰转下划线:", stringx.Camel2Case("GetUserName")) + fmt.Println("通配符匹配:", stringx.IsMatchGreed("hello.txt", "*.txt")) + + // 校验 + fmt.Println("手机号 13812345678:", stringx.CheckMobile("13812345678")) + fmt.Println("身份证 110101199001011234:", stringx.CheckIdCard("110101199001011234")) + + // 中文拼音首字母 + fmt.Println("拼音首字母:", stringx.GetFirstLetter("中国")) + + // 切片删除指定下标元素 + s := []int{1, 2, 3, 4, 5} + s = stringx.DeleteElement(s, 2) + fmt.Println("删除下标2后:", s) +} diff --git a/go.mod b/go.mod index aebcb7d..a705f05 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,9 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible github.com/mattn/go-colorable v0.1.15 + github.com/mozillazg/go-pinyin v0.21.0 github.com/sirupsen/logrus v1.10.2 + golang.org/x/text v0.41.0 gorm.io/gorm v1.25.12 ) @@ -46,6 +48,5 @@ require ( golang.org/x/crypto v0.55.0 // indirect golang.org/x/net v0.57.0 // indirect golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.41.0 // indirect google.golang.org/protobuf v1.36.10 // indirect ) diff --git a/go.sum b/go.sum index f1c4185..63729f2 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mozillazg/go-pinyin v0.21.0 h1:Wo8/NT45z7P3er/9YSLHA3/kjZzbLz5hR7i+jGeIGao= +github.com/mozillazg/go-pinyin v0.21.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/stringx/README.md b/stringx/README.md new file mode 100644 index 0000000..018b335 --- /dev/null +++ b/stringx/README.md @@ -0,0 +1,51 @@ +# stringx + +字符串与切片通用工具:随机串、UUID、通配符匹配、中间串提取、手机号/身份证校验、 +链式字节缓冲、驼峰转下划线、切片删元素、中文拼音首字母。 + +> 迁移自 go-hua/common(原 string.go / array.go / pinyin.go)。 + +## 用法 + +```go +import "git.zeroonesoft.cn/golib/zogo/stringx" + +// 随机串(字母数字 / 纯数字 / 十六进制) +stringx.CreateRandomString(16) +stringx.CreateRandomStringInt(6) +stringx.CreateRandomStringHex(8) + +// GUID(带连字符)/ UUID(不带连字符),基于 crypto/rand +stringx.GetGUID() +stringx.GetUUID() + +// 字符串处理 +stringx.GetBetweenStr("key=value&x=1", "key=", "&") // "value" +stringx.Camel2Case("GetUserName") // "get_user_name" +stringx.IsMatchGreed("hello.txt", "*.txt") // true(另有 IsMatchDynamic) + +// 校验 +stringx.CheckMobile("13812345678") +stringx.CheckIdCard("110101199001011234") +stringx.PhoneCheckRule() // 返回规则正则 +stringx.EmailCheckRule() + +// 中文拼音首字母(非中文字符原样返回) +stringx.GetFirstLetter("中国") // "zg" + +// 切片删除指定下标元素(泛型) +s := stringx.DeleteElement([]int{1, 2, 3, 4, 5}, 2) // [1 2 4 5] + +// 链式字节缓冲 +b := stringx.NewBuffer() +b.Append("a").Append(1).Append([]byte("b")).Append(rune('c')) +``` + +完整可运行例程:[examples/stringx/main.go](../examples/stringx/main.go) + +## 注意 + +- `CreateRandomString*` 内部 `rand.Int` 失败时该位为空字符,正常环境不会发生。 +- `GetGUID/GetUUID` 是随机 UUID(v4 风格),非确定性 UUID;需要标准 RFC 4122 + 格式请用 `github.com/google/uuid`。 +- 通配符匹配仅支持 `*` 和 `?` 两个通配符。 diff --git a/stringx/array.go b/stringx/array.go new file mode 100644 index 0000000..514e670 --- /dev/null +++ b/stringx/array.go @@ -0,0 +1,12 @@ +package stringx + +// DeleteElement 删除切片中指定下标的元素(泛型,支持任意切片类型) +func DeleteElement[S []T, T any](slice S, index int) S { + if index < 0 || index >= len(slice) { + return slice + } + // 计算要删除的元素后面的元素需要向前移动多少位置 + n := len(slice) - 1 + copy(slice[index:n], slice[index+1:n+1]) + return slice[:n] +} diff --git a/stringx/pinyin.go b/stringx/pinyin.go new file mode 100644 index 0000000..e1b8758 --- /dev/null +++ b/stringx/pinyin.go @@ -0,0 +1,26 @@ +package stringx + +import ( + "strings" + + "github.com/mozillazg/go-pinyin" +) + +// GetFirstLetter 获取中文首字母(非中文字符原样返回) +func GetFirstLetter(value string) string { + // 默认 + py := pinyin.NewArgs() + py.Style = pinyin.FirstLetter + py.Fallback = func(r rune, a pinyin.Args) []string { + return []string{string(r)} + } + retText := "" + result := pinyin.Pinyin(value, py) + for _, res := range result { + for _, s := range res { + retText += strings.ToLower(s) + break + } + } + return retText +} diff --git a/stringx/stringx.go b/stringx/stringx.go new file mode 100644 index 0000000..0719ccf --- /dev/null +++ b/stringx/stringx.go @@ -0,0 +1,257 @@ +// Package stringx 字符串与切片通用工具:随机串、UUID、通配符匹配、中间串提取、 +// 手机号/身份证校验、链式字节缓冲、驼峰转下划线、切片删元素、中文拼音首字母。 +package stringx + +import ( + "bytes" + "crypto/rand" + "fmt" + "log" + "math/big" + "regexp" + "strconv" + "strings" + "unicode" +) + +// CreateRandomString 生成指定长度的随机字符串(含大小写字母与数字) +func CreateRandomString(len int) string { + var container string + var str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" + b := bytes.NewBufferString(str) + length := b.Len() + bigInt := big.NewInt(int64(length)) + for i := 0; i < len; i++ { + randomInt, _ := rand.Int(rand.Reader, bigInt) + container += string(str[randomInt.Int64()]) + } + return container +} + +// CreateRandomStringHex 生成指定长度的随机字符串(十六进制字符) +func CreateRandomStringHex(len int) string { + var container string + var str = "ABCDEF1234567890" + b := bytes.NewBufferString(str) + length := b.Len() + bigInt := big.NewInt(int64(length)) + for i := 0; i < len; i++ { + randomInt, _ := rand.Int(rand.Reader, bigInt) + container += string(str[randomInt.Int64()]) + } + return container +} + +// CreateRandomStringInt 生成指定长度的纯数字随机字符串 +func CreateRandomStringInt(len int) string { + var container string + var str = "1234567890" + b := bytes.NewBufferString(str) + length := b.Len() + bigInt := big.NewInt(int64(length)) + for i := 0; i < len; i++ { + randomInt, _ := rand.Int(rand.Reader, bigInt) + container += string(str[randomInt.Int64()]) + } + return container +} + +// IsMatchDynamic 动态规划;时间复杂度O(n^2),空间复杂度O(n^2) +func IsMatchDynamic(s string, p string) bool { + n, m := len(s), len(p) + dp := make([][]bool, n+1) + for i := 0; i <= n; i++ { + dp[i] = make([]bool, m+1) + } + dp[0][0] = true + for i := 1; i <= m; i++ { + if p[i-1] == '*' { + // 可以匹配任意字符串(包括空字符串) + dp[0][i] = true + } else { + break + } + } + for i := 1; i <= n; i++ { + for j := 1; j <= m; j++ { + if p[j-1] == '*' { + // dp[i][j-1]=>不使用这个*,dp[i-1][j]=>使用这个* + dp[i][j] = dp[i][j-1] || dp[i-1][j] + } else if p[j-1] == '?' || s[i-1] == p[j-1] { + dp[i][j] = dp[i-1][j-1] + } + } + } + return dp[n][m] +} + +// IsMatchGreed 贪心;时间复杂度O(n^2),空间复杂度O(1) +func IsMatchGreed(s string, p string) bool { + i, j := 0, 0 + start, last := 0, 0 + for i = 0; i < len(s); { + if j < len(p) && (s[i] == p[j] || p[j] == '?') { + i++ + j++ + } else if j < len(p) && p[j] == '*' { + last = i + // 记录s下一个的位置 + start = j + 1 + // 记录*下一个的位置 + j++ + } else if start != 0 { + last++ + i = last + // 更新到s记录位置的下一个 + j = start + } else { + return false + } + } + for ; j < len(p) && p[j] == '*'; j++ { + + } + return j == len(p) +} + +// GetBetweenStr 取中间字符串 +func GetBetweenStr(str, starting, ending string) string { + s := strings.Index(str, starting) + if s < 0 { + return "" + } + s += len(starting) + if ending == "" { + return str[s:] + } + e := strings.Index(str[s:], ending) + + if e < 0 { + return str[s:] + } + return str[s : s+e] +} + +// GetGUID 使用来自 crypto/rand 包的 rand.Read 函数来生成基本的 UUID。 +func GetGUID() string { + b := make([]byte, 16) + _, err := rand.Read(b) + if err != nil { + return "" + } + uuid := fmt.Sprintf("%x-%x-%x-%x-%x", + b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) + return uuid +} + +// GetUUID 使用来自 crypto/rand 包的 rand.Read 函数来生成基本的 UUID。 +func GetUUID() string { + b := make([]byte, 16) + _, err := rand.Read(b) + if err != nil { + return "" + } + uuid := fmt.Sprintf("%x%x%x%x%x", + b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) + return uuid +} + +// CheckMobile 检验手机号 +func CheckMobile(phone string) bool { + // 匹配规则 + // ^1第一位为一 + // [345789]{1} 后接一位345789 的数字 + // \\d \d的转义 表示数字 {9} 接9位 + // $ 结束符 + regRuler := "^1[345789]{1}\\d{9}$" + + // 正则调用规则 + reg := regexp.MustCompile(regRuler) + + // 返回 MatchString 是否匹配 + return reg.MatchString(phone) +} + +// PhoneCheckRule 手机号验证规则 +func PhoneCheckRule() string { + return "^1[3|4|5|6|7|8|9][0-9]\\d{8}$" +} + +// EmailCheckRule 邮箱验证规则 +func EmailCheckRule() string { + return "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$" +} + +// CheckIdCard 检验身份证 +func CheckIdCard(card string) bool { + //18位身份证 ^(\d{17})([0-9]|X)$ + // 匹配规则 + // (^\d{15}$) 15位身份证 + // (^\d{18}$) 18位身份证 + // (^\d{17}(\d|X|x)$) 18位身份证 最后一位为X的用户 + regRuler := "(^\\d{15}$)|(^\\d{18}$)|(^\\d{17}(\\d|X|x)$)" + + // 正则调用规则 + reg := regexp.MustCompile(regRuler) + + // 返回 MatchString 是否匹配 + return reg.MatchString(card) +} + +// Buffer 内嵌bytes.Buffer,支持连写 +type Buffer struct { + *bytes.Buffer +} + +// NewBuffer 创建空字节缓冲 +func NewBuffer() *Buffer { + return &Buffer{Buffer: new(bytes.Buffer)} +} + +// Append 追加任意类型数据并返回自身(支持链式调用) +func (b *Buffer) Append(i interface{}) *Buffer { + switch val := i.(type) { + case int: + b.append(strconv.Itoa(val)) + case int64: + b.append(strconv.FormatInt(val, 10)) + case uint: + b.append(strconv.FormatUint(uint64(val), 10)) + case uint64: + b.append(strconv.FormatUint(val, 10)) + case string: + b.append(val) + case []byte: + b.Write(val) + case rune: + b.WriteRune(val) + } + return b +} + +// append 追加字符串(容量不足时自动扩容) +func (b *Buffer) append(s string) *Buffer { + defer func() { + if err := recover(); err != nil { + log.Println("*****内存不够了!******") + } + }() + b.WriteString(s) + return b +} + +// Camel2Case 驼峰式写法转为下划线写法 +func Camel2Case(name string) string { + buffer := NewBuffer() + for i, r := range name { + if unicode.IsUpper(r) { + if i != 0 { + buffer.Append('_') + } + buffer.Append(unicode.ToLower(r)) + } else { + buffer.Append(r) + } + } + return buffer.String() +} diff --git a/stringx/stringx_test.go b/stringx/stringx_test.go new file mode 100644 index 0000000..8d6bc68 --- /dev/null +++ b/stringx/stringx_test.go @@ -0,0 +1,141 @@ +package stringx + +import ( + "strings" + "testing" +) + +func TestCreateRandomString(t *testing.T) { + s := CreateRandomString(16) + if len(s) != 16 { + t.Errorf("CreateRandomString(16) 长度 = %d, want 16", len(s)) + } + for _, c := range s { + if !strings.ContainsRune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", c) { + t.Errorf("字符 %q 不在允许字符集内", c) + } + } + if s2 := CreateRandomString(16); s2 == s { + t.Error("两次生成结果不应相同") + } +} + +func TestCreateRandomStringInt(t *testing.T) { + s := CreateRandomStringInt(8) + if len(s) != 8 { + t.Errorf("长度 = %d, want 8", len(s)) + } + for _, c := range s { + if c < '0' || c > '9' { + t.Errorf("字符 %q 应为数字", c) + } + } +} + +func TestCreateRandomStringHex(t *testing.T) { + s := CreateRandomStringHex(8) + if len(s) != 8 { + t.Errorf("长度 = %d, want 8", len(s)) + } + for _, c := range s { + if !strings.ContainsRune("ABCDEF1234567890", c) { + t.Errorf("字符 %q 不在十六进制字符集内", c) + } + } +} + +func TestGetBetweenStr(t *testing.T) { + if got := GetBetweenStr("key=value&x=1", "key=", "&"); got != "value" { + t.Errorf("GetBetweenStr = %q, want value", got) + } + if got := GetBetweenStr("abc", "x", "y"); got != "" { + t.Errorf("起始不存在应返回空: %q", got) + } + if got := GetBetweenStr("a<..>b", "<..>", ""); got != "b" { + t.Errorf("结束为空应返回到结尾: %q", got) + } +} + +func TestGetGUIDFormat(t *testing.T) { + guid := GetGUID() + if strings.Count(guid, "-") != 4 { + t.Errorf("GetGUID 应为 8-4-4-4-12 格式: %q", guid) + } + if len(strings.ReplaceAll(guid, "-", "")) != 32 { + t.Errorf("GetGUID 总长应为 32 位十六进制: %q", guid) + } + uuid := GetUUID() + if strings.Contains(uuid, "-") { + t.Errorf("GetUUID 不应含连字符: %q", uuid) + } +} + +func TestCheckMobile(t *testing.T) { + if !CheckMobile("13812345678") { + t.Error("合法手机号应通过") + } + if CheckMobile("12345678901") { + t.Error("第二位为 2 应不通过") + } + if CheckMobile("1381234567") { + t.Error("10 位应不通过") + } +} + +func TestCheckIdCard(t *testing.T) { + if !CheckIdCard("110101199001011234") { + t.Error("18 位数字应通过") + } + if !CheckIdCard("11010119900101123X") { + t.Error("17 位数字 + X 应通过") + } + if CheckIdCard("abc") { + t.Error("非法字符应不通过") + } +} + +func TestIsMatch(t *testing.T) { + if !IsMatchGreed("hello.txt", "*.txt") { + t.Error("IsMatchGreed(hello.txt, *.txt) 应为 true") + } + if IsMatchGreed("hello.txt", "*.jpg") { + t.Error("IsMatchGreed(hello.txt, *.jpg) 应为 false") + } + if !IsMatchDynamic("hello.txt", "*.txt") { + t.Error("IsMatchDynamic(hello.txt, *.txt) 应为 true") + } +} + +func TestCamel2Case(t *testing.T) { + if got := Camel2Case("GetUserName"); got != "get_user_name" { + t.Errorf("Camel2Case = %q, want get_user_name", got) + } + if got := Camel2Case("id"); got != "id" { + t.Errorf("Camel2Case = %q, want id", got) + } +} + +func TestDeleteElement(t *testing.T) { + s := []int{1, 2, 3, 4, 5} + got := DeleteElement(s, 2) + if len(got) != 4 || got[0] != 1 || got[1] != 2 || got[2] != 4 || got[3] != 5 { + t.Errorf("DeleteElement = %v, want [1 2 4 5]", got) + } +} + +func TestBuffer(t *testing.T) { + b := NewBuffer() + b.Append("a").Append(1).Append(int64(2)).Append([]byte("b")).Append(uint64(3)) + if b.String() != "a12b3" { + t.Errorf("Buffer = %q, want a12b3", b.String()) + } +} + +func TestGetFirstLetter(t *testing.T) { + if got := GetFirstLetter("中国"); got != "zg" { + t.Errorf("GetFirstLetter(中国) = %q, want zg", got) + } + if got := GetFirstLetter("abc123"); got != "abc123" { + t.Errorf("非中文字符应原样返回: %q", got) + } +}