feat(stringx): 自 go-hua/common 迁入字符串/切片/拼音工具并独立成包
- 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)
This commit is contained in:
@@ -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`。
|
||||
- 通配符匹配仅支持 `*` 和 `?` 两个通配符。
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user