Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
# String Util
This is a go string operate util package.
- Github: https://github.com/gookit/goutil/strutil
- GoDoc: https://godoc.org/github.com/gookit/goutil/strutil
## Install
```bash
go get github.com/gookit/goutil/strutil
```
## Usage
```go
ss := strutil.ToArray("a,b,c", ",")
// Output: []string{"a", "b", "c"}
ints, err := strutil.ToIntSlice("1,2,3")
// Output: []int{1, 2, 3}
```
## Functions
```go
func AddSlashes(s string) string
func AnyToString(val interface{}, defaultAsErr bool) (str string, err error)
func B32Decode(str string) string
func B32Encode(str string) string
func B64Decode(str string) string
func B64Encode(str string) string
func Base64(str string) string
func Bool(s string) (bool, error)
func Byte2str(b []byte) string
func Byte2string(b []byte) string
func BytePos(s string, bt byte) int
func Camel(s string, sep ...string) string
func CamelCase(s string, sep ...string) string
func Compare(s1, s2, op string) bool
func Cut(s, sep string) (before string, after string, found bool)
func EscapeHTML(s string) string
func EscapeJS(s string) string
func FilterEmail(s string) string
func GenMd5(src interface{}) string
func HasAllSubs(s string, subs []string) bool
func HasOnePrefix(s string, prefixes []string) bool
func HasOneSub(s string, subs []string) bool
func HasPrefix(s string, prefix string) bool
func HasSuffix(s string, suffix string) bool
func Implode(sep string, ss ...string) string
func Indent(s, prefix string) string
func IndentBytes(b, prefix []byte) []byte
func Int(s string) (int, error)
func Int64(s string) int64
func Int64OrErr(s string) (int64, error)
func Int64OrPanic(s string) int64
func IntOrPanic(s string) int
func Ints(s string, sep ...string) []int
func IsAlphaNum(c uint8) bool
func IsAlphabet(char uint8) bool
func IsBlank(s string) bool
func IsBlankBytes(bs []byte) bool
func IsEmpty(s string) bool
func IsEndOf(s, suffix string) bool
func IsNotBlank(s string) bool
func IsNumChar(c byte) bool
func IsNumeric(s string) bool
func IsSpace(c byte) bool
func IsSpaceRune(r rune) bool
func IsStartOf(s, prefix string) bool
func IsStartsOf(s string, prefixes []string) bool
func IsSymbol(r rune) bool
func IsValidUtf8(s string) bool
func IsVersion(s string) bool
func Join(sep string, ss ...string) string
func JoinList(sep string, ss []string) string
func LTrim(s string, cutSet ...string) string
func Lower(s string) string
func LowerFirst(s string) string
func Lowercase(s string) string
func Ltrim(s string, cutSet ...string) string
func MD5(src interface{}) string
func Md5(src interface{}) string
func MicroTimeHexID() string
func MicroTimeID() string
func MustBool(s string) bool
func MustCut(s, sep string) (before string, after string)
func MustInt(s string) int
func MustInt64(s string) int64
func MustString(in interface{}) string
func MustToTime(s string, layouts ...string) time.Time
func NoCaseEq(s, t string) bool
func PadLeft(s, pad string, length int) string
func PadRight(s, pad string, length int) string
func Padding(s, pad string, length int, pos uint8) string
func PrettyJSON(v interface{}) (string, error)
func QuietBool(s string) bool
func QuietInt(s string) int
func QuietInt64(s string) int64
func QuietString(in interface{}) string
func Quote(s string) string
func RTrim(s string, cutSet ...string) string
func RandomBytes(length int) ([]byte, error)
func RandomChars(ln int) string
func RandomCharsV2(ln int) string
func RandomCharsV3(ln int) string
func RandomString(length int) (string, error)
func RenderTemplate(input string, data interface{}, fns template.FuncMap, isFile ...bool) string
func RenderText(input string, data interface{}, fns template.FuncMap, isFile ...bool) string
func Repeat(s string, times int) string
func RepeatBytes(char byte, times int) (chars []byte)
func RepeatRune(char rune, times int) (chars []rune)
func Replaces(str string, pairs map[string]string) string
func Rtrim(s string, cutSet ...string) string
func RuneCount(s string) int
func RuneIsLower(c rune) bool
func RuneIsUpper(c rune) bool
func RuneIsWord(c rune) bool
func RunePos(s string, ru rune) int
func RuneWidth(r rune) int
func Similarity(s, t string, rate float32) (float32, bool)
func SnakeCase(s string, sep ...string) string
func Split(s, sep string) (ss []string)
func SplitInlineComment(val string) (string, string)
func SplitN(s, sep string, n int) (ss []string)
func SplitNTrimmed(s, sep string, n int) (ss []string)
func SplitNValid(s, sep string, n int) (ss []string)
func SplitTrimmed(s, sep string) (ss []string)
func SplitValid(s, sep string) (ss []string)
func StrPos(s, sub string) int
func String(val interface{}) (string, error)
func StringOrErr(val interface{}) (string, error)
func Strings(s string, sep ...string) []string
func StripSlashes(s string) string
func Substr(s string, pos, length int) string
func TextSplit(s string, w int) []string
func TextTruncate(s string, w int, tail string) string
func TextWidth(s string) int
func TextWrap(s string, w int) string
func Title(s string) string
func ToArray(s string, sep ...string) []string
func ToBool(s string) (bool, error)
func ToBytes(s string) (b []byte)
func ToDuration(s string) (time.Duration, error)
func ToInt(s string) (int, error)
func ToInt64(s string) (int64, error)
func ToIntSlice(s string, sep ...string) (ints []int, err error)
func ToInts(s string, sep ...string) ([]int, error)
func ToSlice(s string, sep ...string) []string
func ToString(val interface{}) (string, error)
func ToStrings(s string, sep ...string) []string
func ToTime(s string, layouts ...string) (t time.Time, err error)
func Trim(s string, cutSet ...string) string
func TrimCut(s, sep string) (string, string)
func TrimLeft(s string, cutSet ...string) string
func TrimRight(s string, cutSet ...string) string
func URLDecode(s string) string
func URLEncode(s string) string
func Unquote(s string) string
func Upper(s string) string
func UpperFirst(s string) string
func UpperWord(s string) string
func Uppercase(s string) string
func Utf8Len(s string) int
func Utf8Split(s string, w int) []string
func Utf8Truncate(s string, w int, tail string) string
func Utf8Width(s string) (size int)
func Utf8len(s string) int
func VersionCompare(v1, v2, op string) bool
func WidthWrap(s string, w int) string
func WrapTag(s, tag string) string
```
## Code Check & Testing
```bash
gofmt -w -l ./
golint ./...
```
**Testing**:
```shell
go test -v ./strutil/...
```
**Test limit by regexp**:
```shell
go test -v -run ^TestSetByKeys ./strutil/...
```
+33
View File
@@ -0,0 +1,33 @@
package strutil
import (
"github.com/gookit/goutil/byteutil"
)
// Buffer wrap and extends the bytes.Buffer
type Buffer = byteutil.Buffer
// NewBuffer instance, can set init size
func NewBuffer(initSize ...int) *Buffer {
if len(initSize) > 0 && initSize[0] > 0 {
buf := &Buffer{}
buf.Grow(initSize[0])
return buf
}
return &Buffer{}
}
// ByteChanPool struct
//
// Usage:
//
// bp := strutil.NewByteChanPool(500, 1024, 1024)
// buf:=bp.Get()
// defer bp.Put(buf)
// // use buf do something ...
type ByteChanPool = byteutil.ChanPool
// NewByteChanPool instance
func NewByteChanPool(maxSize, width, capWidth int) *ByteChanPool {
return byteutil.NewChanPool(maxSize, width, capWidth)
}
+513
View File
@@ -0,0 +1,513 @@
package strutil
import (
"path"
"regexp"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"github.com/gookit/goutil/internal/checkfn"
)
// Equal check, alias of strings.EqualFold
var Equal = strings.EqualFold
var IsHttpURL = checkfn.IsHttpURL
// IsNumChar returns true if the given character is a numeric, otherwise false.
func IsNumChar(c byte) bool { return c >= '0' && c <= '9' }
var (
uintReg = regexp.MustCompile(`^\d+$`)
intReg = regexp.MustCompile(`^[-+]?\d+$`)
floatReg = regexp.MustCompile(`^[-+]?\d*\.?\d+$`)
)
// IsInt check the string is an integer number
func IsInt(s string) bool {
if s == "" {
return false
}
return intReg.MatchString(s)
}
// IsUint check the string is an unsigned integer number
func IsUint(s string) bool {
if s == "" {
return false
}
return uintReg.MatchString(s)
}
// IsFloat check the string is a float number
func IsFloat(s string) bool {
if s == "" {
return false
}
return floatReg.MatchString(s)
}
// IsNumeric returns true if the given string is a numeric(int/float), otherwise false.
func IsNumeric(s string) bool { return checkfn.IsNumeric(s) }
// IsPositiveNum check the string is a positive number
func IsPositiveNum(s string) bool { return checkfn.IsPositiveNum(s) }
// IsAlphabet char
func IsAlphabet(char uint8) bool {
// A 65 -> Z 90
if char >= 'A' && char <= 'Z' {
return true
}
// a 97 -> z 122
if char >= 'a' && char <= 'z' {
return true
}
return false
}
// IsAlphaNum reports whether the byte is an ASCII letter, number, or underscore
func IsAlphaNum(c uint8) bool {
return c == '_' || '0' <= c && c <= '9' || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z'
}
// IsUpper returns true if the given string is an uppercase, otherwise false.
func IsUpper(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] >= 'A' && s[i] <= 'Z' {
continue
}
return false
}
return true
}
// IsLower returns true if the given string is a lowercase, otherwise false.
func IsLower(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] >= 'a' && s[i] <= 'z' {
continue
}
return false
}
return true
}
// StrPos alias of the strings.Index
func StrPos(s, sub string) int { return strings.Index(s, sub) }
// BytePos alias of the strings.IndexByte
func BytePos(s string, bt byte) int { return strings.IndexByte(s, bt) }
// IEqual ignore case check given two strings are equals.
func IEqual(s1, s2 string) bool { return strings.EqualFold(s1, s2) }
// NoCaseEq check two strings is equals and case-insensitivity
func NoCaseEq(s, t string) bool { return strings.EqualFold(s, t) }
// IContains ignore case check substr in the given string.
func IContains(s, sub string) bool {
return strings.Contains(strings.ToLower(s), strings.ToLower(sub))
}
// ContainsByte in given string.
func ContainsByte(s string, c byte) bool { return strings.IndexByte(s, c) >= 0 }
// ContainsByteOne in given string.
func ContainsByteOne(s string, bs []byte) bool {
for _, b := range bs {
if strings.IndexByte(s, b) >= 0 {
return true
}
}
return false
}
// InArray alias of HasOneSub()
var InArray = HasOneSub
// ContainsOne substr(s) in the given string. alias of HasOneSub()
func ContainsOne(s string, subs []string) bool { return HasOneSub(s, subs) }
// HasOneSub substr(s) in the given string.
func HasOneSub(s string, subs []string) bool {
for _, sub := range subs {
if strings.Contains(s, sub) {
return true
}
}
return false
}
// IContainsOne ignore case check has one substr(s) in the given string.
func IContainsOne(s string, subs []string) bool {
s = strings.ToLower(s)
for _, sub := range subs {
if strings.Contains(s, strings.ToLower(sub)) {
return true
}
}
return false
}
// ContainsAll given string should contain all substrings. alias of HasAllSubs()
func ContainsAll(s string, subs []string) bool { return HasAllSubs(s, subs) }
// HasAllSubs given string should contain all substrings
func HasAllSubs(s string, subs []string) bool {
for _, sub := range subs {
if !strings.Contains(s, sub) {
return false
}
}
return true
}
// IContainsAll like ContainsAll(), but ignore case
func IContainsAll(s string, subs []string) bool {
s = strings.ToLower(s)
for _, sub := range subs {
if !strings.Contains(s, strings.ToLower(sub)) {
return false
}
}
return true
}
// StartsWithAny alias of the HasOnePrefix
var StartsWithAny = HasOneSuffix
// IsStartsOf alias of the HasOnePrefix
func IsStartsOf(s string, prefixes []string) bool {
return HasOnePrefix(s, prefixes)
}
// HasOnePrefix the string starts with one of the subs
func HasOnePrefix(s string, prefixes []string) bool {
for _, prefix := range prefixes {
if len(s) >= len(prefix) && s[0:len(prefix)] == prefix {
return true
}
}
return false
}
// StartsWith alias func for HasPrefix
var StartsWith = strings.HasPrefix
// HasPrefix substr in the given string.
func HasPrefix(s string, prefix string) bool { return strings.HasPrefix(s, prefix) }
// IsStartOf alias of the strings.HasPrefix
func IsStartOf(s, prefix string) bool { return strings.HasPrefix(s, prefix) }
// HasSuffix substr in the given string.
func HasSuffix(s string, suffix string) bool { return strings.HasSuffix(s, suffix) }
// IsEndOf alias of the strings.HasSuffix
func IsEndOf(s, suffix string) bool { return strings.HasSuffix(s, suffix) }
// HasOneSuffix the string end with one of the subs
func HasOneSuffix(s string, suffixes []string) bool {
for _, suffix := range suffixes {
if strings.HasSuffix(s, suffix) {
return true
}
}
return false
}
// IsValidUtf8 valid utf8 string check
func IsValidUtf8(s string) bool { return utf8.ValidString(s) }
// ----- refer from github.com/yuin/goldmark/util
// refer from github.com/yuin/goldmark/util
var spaceTable = [256]int8{
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
}
// IsSpace returns true if the given character is a space, otherwise false.
func IsSpace(c byte) bool { return spaceTable[c] == 1 }
// IsEmpty returns true if the given string is empty.
func IsEmpty(s string) bool { return len(s) == 0 }
// IsBlank returns true if the given string is all space characters.
func IsBlank(s string) bool { return IsBlankBytes([]byte(s)) }
// IsNotBlank returns true if the given string is not blank.
func IsNotBlank(s string) bool { return !IsBlankBytes([]byte(s)) }
// IsBlankBytes returns true if the given []byte is all space characters.
func IsBlankBytes(bs []byte) bool {
for _, b := range bs {
if !IsSpace(b) {
return false
}
}
return true
}
// IsSymbol reports whether the rune is a symbolic character.
func IsSymbol(r rune) bool { return unicode.IsSymbol(r) }
// HasEmpty value for input strings
func HasEmpty(ss ...string) bool {
for _, s := range ss {
if s == "" {
return true
}
}
return false
}
// IsAllEmpty for input strings
func IsAllEmpty(ss ...string) bool {
for _, s := range ss {
if s != "" {
return false
}
}
return true
}
var (
// regex for check version number
verRegex = regexp.MustCompile(`^[0-9][\d.]+(-\w+)?$`)
// regex for check variable name
varRegex = regexp.MustCompile(`^[a-zA-Z][\w-]*$`)
// regex for check env var name
envRegex = regexp.MustCompile(`^[A-Z][A-Z0-9_]*$`)
// IsVariableName alias for IsVarName
IsVariableName = IsVarName
// regex for check uuid string. format: 8-4-4-4-12
uuidPattern = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
)
// IsVersion number. eg: 1.2.0
func IsVersion(s string) bool { return verRegex.MatchString(s) }
// IsVarName is valid variable name.
func IsVarName(s string) bool { return varRegex.MatchString(s) }
// IsEnvName is valid ENV var name. eg: APP_NAME
func IsEnvName(s string) bool { return envRegex.MatchString(s) }
// IsUUID check if the string is a valid UUID format.
func IsUUID(s string) bool { return uuidPattern.MatchString(s) }
// Compare for two strings.
func Compare(s1, s2, op string) bool {
switch op {
case ">", "gt":
return s1 > s2
case "<", "lt":
return s1 < s2
case ">=", "gte":
return s1 >= s2
case "<=", "lte":
return s1 <= s2
case "!=", "ne", "neq":
return s1 != s2
default: // eq
return s1 == s2
}
}
// VersionCompare for two version strings. eg: 1.2.0 > 1.1.0
func VersionCompare(v1, v2, op string) bool {
parts1 := parseVersion(v1)
parts2 := parseVersion(v2)
result := compareVersions(parts1, parts2)
switch op {
case ">", "gt":
return result > 0
case "<", "lt":
return result < 0
case "=", "==", "eq":
return result == 0
case "!=", "ne", "neq":
return result != 0
case ">=", "gte":
return result >= 0
case "<=", "lte":
return result <= 0
default:
return false
}
}
// parseVersion 将版本号字符串解析为整数数组
func parseVersion(version string) []int {
parts := strings.Split(version, ".")
result := make([]int, len(parts))
for i, part := range parts {
num, _ := strconv.Atoi(part)
result[i] = num
}
return result
}
// compareVersions 比较两个版本号数组
// 返回: -1 表示 v1 < v2, 0 表示 v1 = v2, 1 表示 v1 > v2
func compareVersions(v1, v2 []int) int {
maxLen := len(v1)
if len(v2) > maxLen {
maxLen = len(v2)
}
for i := 0; i < maxLen; i++ {
num1 := 0
if i < len(v1) {
num1 = v1[i]
}
num2 := 0
if i < len(v2) {
num2 = v2[i]
}
if num1 > num2 {
return 1
} else if num1 < num2 {
return -1
}
}
return 0
}
// SimpleMatch all substring in the give text string.
//
// Difference the ContainsAll:
//
// - start with ^ for exclude contains check.
// - end with $ for the check end with keyword.
func SimpleMatch(s string, keywords []string) bool {
for _, keyword := range keywords {
kln := len(keyword)
if kln == 0 {
continue
}
// exclude
if kln > 1 && keyword[0] == '^' {
if strings.Contains(s, keyword[1:]) {
return false
}
continue
}
// end with
if kln > 1 && keyword[kln-1] == '$' {
return strings.HasSuffix(s, keyword[:kln-1])
}
// include
if !strings.Contains(s, keyword) {
return false
}
}
return true
}
// QuickMatch check for a string. pattern can be a substring.
func QuickMatch(pattern, s string) bool {
if strings.ContainsRune(pattern, '*') {
return GlobMatch(pattern, s)
}
return strings.Contains(s, pattern)
}
// PathMatch check for a string match the pattern. alias of the path.Match()
//
// TIP: `*` can match any char, not contain `/`.
func PathMatch(pattern, s string) bool {
ok, err := path.Match(pattern, s)
if err != nil {
ok = false
}
return ok
}
// GlobMatch check for a string match the pattern.
//
// Difference with PathMatch() is: `*` can match any char, contain `/`.
func GlobMatch(pattern, s string) bool {
// replace `/` to `S` for path.Match
pattern = strings.Replace(pattern, "/", "S", -1)
s = strings.Replace(s, "/", "S", -1)
ok, err := path.Match(pattern, s)
if err != nil {
ok = false
}
return ok
}
// LikeMatch simple check for a string match the pattern. pattern like the SQL LIKE.
func LikeMatch(pattern, s string) bool {
ln := len(pattern)
if ln < 2 {
return false
}
// eg `%abc` `%abc%`
if pattern[0] == '%' {
if ln > 2 && pattern[ln-1] == '%' {
return strings.Contains(s, pattern[1:ln-1])
}
return strings.HasSuffix(s, pattern[1:])
}
// eg `abc%`
if pattern[ln-1] == '%' {
return strings.HasPrefix(s, pattern[:ln-1])
}
return pattern == s
}
// MatchNodePath check for a string match the pattern.
//
// Use on a pattern:
// - `*` match any to sep
// - `**` match any to end. only allow at start or end on pattern.
//
// Example:
//
// strutil.MatchNodePath()
func MatchNodePath(pattern, s string, sep string) bool {
if pattern == "**" || pattern == s {
return true
}
if pattern == "" {
return len(s) == 0
}
if i := strings.Index(pattern, "**"); i >= 0 {
if i == 0 { // at start
return strings.HasSuffix(s, pattern[2:])
}
return strings.HasPrefix(s, pattern[:len(pattern)-2])
}
pattern = strings.Replace(pattern, sep, "/", -1)
s = strings.Replace(s, sep, "/", -1)
ok, err := path.Match(pattern, s)
if err != nil {
ok = false
}
return ok
}
+108
View File
@@ -0,0 +1,108 @@
package strutil
import (
"math/big"
"strconv"
"strings"
"github.com/gookit/goutil/x/basefn"
)
//
// -------------------- convert base --------------------
//
const (
Base10Chars = "0123456789"
Base16Chars = "0123456789abcdef"
Base32Chars = "0123456789abcdefghjkmnpqrstvwxyz"
Base36Chars = "0123456789abcdefghijklmnopqrstuvwxyz"
Base48Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL"
Base62Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
Base64Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/"
)
// Base10Conv convert base10 string to new base string.
func Base10Conv(src string, to int) string { return BaseConv(src, 10, to) }
// BaseConv convert base string by from and to base.
//
// Note: from and to base must be in [2, 64]
//
// Usage:
//
// BaseConv("123", 10, 16) // Output: "7b"
// BaseConv("7b", 16, 10) // Output: "123"
func BaseConv(src string, from, to int) string {
if from > 64 || from < 2 {
from = 10
}
if to > 64 || to < 2 {
to = 16
}
return BaseConvByTpl(src, Base64Chars[:from], Base64Chars[:to])
}
// BaseConvInt convert base int to new base string.
//
// Usage:
//
// BaseConv(123, 16) // Output: "7b"
func BaseConvInt(src uint64, toBase int) string {
if toBase > 64 || toBase < 2 {
toBase = 16
}
// bigInt 支持 2-62 进制转换处理 TODO
if toBase <= 36 {
return strconv.FormatUint(src, toBase)
}
if toBase <= 62 {
bigInt := new(big.Int).SetUint64(src)
return bigInt.Text(toBase)
}
return BaseConvIntByTpl(src, Base64Chars[:toBase])
}
// BaseConvByTpl convert base string by template.
//
// Usage:
//
// BaseConvert("123", Base62Chars, Base16Chars) // Output: "1e"
// BaseConvert("1e", Base16Chars, Base62Chars) // Output: "123"
func BaseConvByTpl(src string, fromBase, toBase string) string {
if fromBase == toBase {
return src
}
// convert to base 10
var dec uint64
if fromBase == Base10Chars {
var err error
dec, err = strconv.ParseUint(src, 10, 0)
if err != nil {
basefn.Panicf("input is not a valid decimal number: %s(%v)", src, err)
}
} else {
fLen := uint64(len(fromBase))
for _, c := range src {
dec = dec*fLen + uint64(strings.IndexRune(fromBase, c))
}
}
// convert to new base
return BaseConvIntByTpl(dec, toBase)
}
// BaseConvIntByTpl convert base int to new base string.
func BaseConvIntByTpl(dec uint64, toBase string) string {
// convert to new base
var res string
toLen := uint64(len(toBase))
for dec > 0 {
res = string(toBase[dec%toLen]) + res
dec /= toLen
}
return res
}
+413
View File
@@ -0,0 +1,413 @@
package strutil
import (
"errors"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"unsafe"
"github.com/gookit/goutil/internal/comfunc"
"github.com/gookit/goutil/mathutil"
)
var (
// ErrDateLayout error
ErrDateLayout = errors.New("invalid date layout string")
// ErrInvalidParam error
ErrInvalidParam = errors.New("invalid input for parse time")
// some regex for convert string.
toSnakeReg = regexp.MustCompile("[A-Z][a-z]")
toCamelRegs = map[string]*regexp.Regexp{
" ": regexp.MustCompile(" +[a-zA-Z]"),
"-": regexp.MustCompile("-+[a-zA-Z]"),
"_": regexp.MustCompile("_+[a-zA-Z]"),
}
)
// Internal func refers:
// strconv.QuoteRune()
// strconv.QuoteToASCII()
// strconv.AppendQuote()
// strconv.AppendQuoteRune()
// Quote alias of strings.Quote
func Quote(s string) string { return strconv.Quote(s) }
// Unquote remove start and end quotes by single-quote or double-quote
//
// tip: strconv.Unquote cannot unquote single-quote
func Unquote(s string) string {
ln := len(s)
if ln < 2 {
return s
}
qs, qe := s[0], s[ln-1]
var valid bool
if qs == '"' && qe == '"' {
valid = true
} else if qs == '\'' && qe == '\'' {
valid = true
}
if valid {
s = s[1 : ln-1] // exclude quotes
}
// strconv.Unquote cannot unquote single-quote
// if ns, err := strconv.Unquote(s); err == nil {
// return ns
// }
return s
}
// Join alias of strings.Join
func Join(sep string, ss ...string) string { return strings.Join(ss, sep) }
// JoinList alias of strings.Join
func JoinList(sep string, ss []string) string { return strings.Join(ss, sep) }
// JoinComma quick join strings by comma
func JoinComma(ss []string) string { return strings.Join(ss, ",") }
// JoinAny type to string
func JoinAny(sep string, parts ...any) string {
ss := make([]string, 0, len(parts))
for _, part := range parts {
ss = append(ss, QuietString(part))
}
return strings.Join(ss, sep)
}
// Implode alias of strings.Join
func Implode(sep string, ss ...string) string { return strings.Join(ss, sep) }
/*************************************************************
* region value to string
*************************************************************/
// String convert value to string, return error on failed
func String(val any) (string, error) { return ToStringWith(val) }
// ToString convert value to string, return error on failed
func ToString(val any) (string, error) { return ToStringWith(val) }
// StringOrErr convert value to string, return error on failed
func StringOrErr(val any) (string, error) { return ToStringWith(val) }
// QuietString convert value to string, will ignore error. same as SafeString()
func QuietString(val any) string { return SafeString(val) }
// SafeString convert value to string. Will ignore error
func SafeString(in any) string {
s, _ := AnyToString(in, false)
return s
}
// StringOrPanic convert value to string, will panic on error
func StringOrPanic(val any) string { return MustString(val) }
// MustString convert value to string. will panic on error
func MustString(val any) string {
s, err := ToStringWith(val)
if err != nil {
panic(err)
}
return s
}
// StringOrDefault convert any value to string, return default value on failed
func StringOrDefault(val any, defVal string) string { return StringOr(val, defVal) }
// StringOr convert any value to string, return default value on failed
func StringOr(val any, defVal string) string {
s, err := ToStringWith(val)
if err != nil {
return defVal
}
return s
}
// AnyToString convert any value to string.
//
// For defaultAsErr:
//
// - False will use fmt.Sprint convert unsupported type
// - True will return error on convert fail.
func AnyToString(val any, defaultAsErr bool) (s string, err error) {
var optFn comfunc.ConvOptionFn
if !defaultAsErr {
optFn = comfunc.WithUserConvFn(comfunc.StrBySprintFn)
}
return comfunc.ToStringWith(val, optFn)
}
// ToStringWith try to convert value to string. can with some option func, more see comfunc.ConvOption.
func ToStringWith(in any, optFns ...comfunc.ConvOptionFn) (string, error) {
return comfunc.ToStringWith(in, optFns...)
}
/*************************************************************
* region string value to bool
*************************************************************/
// ToBool convert string to bool
func ToBool(s string) (bool, error) {
return comfunc.StrToBool(strings.TrimSpace(s))
}
// QuietBool convert to bool, will ignore error
func QuietBool(s string) bool { return SafeBool(s) }
// SafeBool convert to bool and will ignore error
func SafeBool(s string) bool {
val, _ := comfunc.StrToBool(strings.TrimSpace(s))
return val
}
// MustBool convert to bool and will panic on error
func MustBool(s string) bool {
val, err := ToBool(s)
if err != nil {
panic(err)
}
return val
}
// Bool parse string to bool. like strconv.ParseBool()
func Bool(s string) (bool, error) {
return comfunc.StrToBool(strings.TrimSpace(s))
}
/*************************************************************
* region string value to int
*************************************************************/
// Int convert string to int, alias of ToInt()
func Int(s string) (int, error) {
return strconv.Atoi(strings.TrimSpace(s))
}
// ToInt convert string to int, return error on fail
func ToInt(s string) (int, error) {
return strconv.Atoi(strings.TrimSpace(s))
}
// IntOrDefault convert string to int, return default value on fail
func IntOrDefault(s string, defVal int) int {
return IntOr(s, defVal)
}
// IntOr convert string to int, return default value on fail
func IntOr(s string, defVal int) int {
val, err := ToInt(s)
if err != nil {
return defVal
}
return val
}
// SafeInt convert string to int, will ignore error
func SafeInt(s string) int {
val, _ := ToInt(s)
return val
}
// QuietInt convert string to int, will ignore error
func QuietInt(s string) int { return SafeInt(s) }
// MustInt convert string to int, will panic on error
func MustInt(s string) int { return IntOrPanic(s) }
// IntOrPanic convert value to int, will panic on error
func IntOrPanic(s string) int {
val, err := ToInt(s)
if err != nil {
panic(err)
}
return val
}
/*************************************************************
* region convert string to int64
*************************************************************/
// Int64 convert string to int, will ignore error
func Int64(s string) int64 { return SafeInt64(s) }
// QuietInt64 convert string to int, will ignore error
func QuietInt64(s string) int64 { return SafeInt64(s) }
// SafeInt64 convert string to int, will ignore error
func SafeInt64(s string) int64 {
val, _ := Int64OrErr(s)
return val
}
// ToInt64 convert string to int, return error on fail
func ToInt64(s string) (int64, error) {
return strconv.ParseInt(s, 10, 0)
}
// Int64OrDefault convert string to int, return default value on fail
func Int64OrDefault(s string, defVal int64) int64 {
return Int64Or(s, defVal)
}
// Int64Or convert string to int, return default value on fail
func Int64Or(s string, defVal int64) int64 {
val, err := ToInt64(s)
if err != nil {
return defVal
}
return val
}
// Int64OrErr convert string to int, return error on fail
func Int64OrErr(s string) (int64, error) {
return strconv.ParseInt(s, 10, 0)
}
// MustInt64 convert value to int, will panic on error
func MustInt64(s string) int64 { return Int64OrPanic(s) }
// Int64OrPanic convert value to int, will panic on error
func Int64OrPanic(s string) int64 {
val, err := strconv.ParseInt(s, 10, 0)
if err != nil {
panic(err)
}
return val
}
/*************************************************************
* region string value to uint
*************************************************************/
// Uint convert string to uint, will ignore error
func Uint(s string) uint64 { return SafeUint(s) }
// SafeUint convert string to uint, will ignore error
func SafeUint(s string) uint64 {
val, _ := UintOrErr(s)
return val
}
// ToUint convert string to uint, return error on fail. alias of UintOrErr()
func ToUint(s string) (uint64, error) {
return strconv.ParseUint(s, 10, 0)
}
// UintOrErr convert string to uint, return error on fail
func UintOrErr(s string) (uint64, error) {
return strconv.ParseUint(s, 10, 0)
}
// MustUint convert value to uint, will panic on error. alias of UintOrPanic()
func MustUint(s string) uint64 { return UintOrPanic(s) }
// UintOrPanic convert value to uint, will panic on error
func UintOrPanic(s string) uint64 {
val, err := UintOrErr(s)
if err != nil {
panic(err)
}
return val
}
// UintOrDefault convert string to uint, return default value on fail
func UintOrDefault(s string, defVal uint64) uint64 {
return UintOr(s, defVal)
}
// UintOr convert string to uint, return default value on fail
func UintOr(s string, defVal uint64) uint64 {
val, err := UintOrErr(s)
if err != nil {
return defVal
}
return val
}
/*************************************************************
* region string value to byte
* refer from https://github.com/valyala/fastjson/blob/master/util.go
*************************************************************/
// Byte2str convert bytes to string
func Byte2str(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
// Byte2string convert bytes to string
func Byte2string(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
// ToBytes convert string to bytes
func ToBytes(s string) (b []byte) {
strh := (*reflect.StringHeader)(unsafe.Pointer(&s))
sh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh.Data = strh.Data
sh.Len = strh.Len
sh.Cap = strh.Len
return b
}
/*************************************************************
* region string to int/string slice, time.Time
*************************************************************/
// Ints alias of the ToIntSlice(). default sep is comma(,)
func Ints(s string, sep ...string) []int {
ints, _ := ToIntSlice(s, sep...)
return ints
}
// ToInts alias of the ToIntSlice(). default sep is comma(,)
func ToInts(s string, sep ...string) ([]int, error) { return ToIntSlice(s, sep...) }
// ToIntSlice split string to slice and convert item to int.
//
// Default sep is comma
func ToIntSlice(s string, sep ...string) (ints []int, err error) {
ss := ToSlice(s, sep...)
for _, item := range ss {
iVal, err := mathutil.ToInt(item)
if err != nil {
return []int{}, err
}
ints = append(ints, iVal)
}
return
}
// ToArray alias of the ToSlice()
func ToArray(s string, sep ...string) []string { return ToSlice(s, sep...) }
// Strings alias of the ToSlice()
func Strings(s string, sep ...string) []string { return ToSlice(s, sep...) }
// ToStrings alias of the ToSlice()
func ToStrings(s string, sep ...string) []string { return ToSlice(s, sep...) }
// ToSlice split string to array.
func ToSlice(s string, sep ...string) []string {
if len(sep) > 0 {
return Split(s, sep[0])
}
return Split(s, ",")
}
// ToDuration parses a duration string. such as "300ms", "-1.5h" or "2h45m".
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
func ToDuration(s string) (time.Duration, error) {
return comfunc.ToDuration(s)
}
+140
View File
@@ -0,0 +1,140 @@
package strutil
import (
"bytes"
"encoding/base32"
"encoding/base64"
"net/url"
"strings"
"text/template"
)
//
// -------------------- escape --------------------
//
// EscapeJS escape javascript string
func EscapeJS(s string) string {
return template.JSEscapeString(s)
}
// EscapeHTML escape html string
func EscapeHTML(s string) string {
return template.HTMLEscapeString(s)
}
// AddSlashes add slashes for the string.
func AddSlashes(s string) string {
if ln := len(s); ln == 0 {
return ""
}
var buf bytes.Buffer
for _, char := range s {
switch char {
case '\'', '"', '\\':
buf.WriteRune('\\')
}
buf.WriteRune(char)
}
return buf.String()
}
// StripSlashes strip slashes for the string.
func StripSlashes(s string) string {
ln := len(s)
if ln == 0 {
return ""
}
var skip bool
var buf bytes.Buffer
for i, char := range s {
if skip {
skip = false
} else if char == '\\' {
if i+1 < ln && s[i+1] == '\\' {
skip = true
}
continue
}
buf.WriteRune(char)
}
return buf.String()
}
//
// -------------------- encode --------------------
//
// URLEncode encode url string.
func URLEncode(s string) string {
if pos := strings.IndexRune(s, '?'); pos > -1 { // escape query data
return s[0:pos+1] + url.QueryEscape(s[pos+1:])
}
return s
}
// URLDecode decode url string.
func URLDecode(s string) string {
if pos := strings.IndexRune(s, '?'); pos > -1 { // un-escape query data
qy, err := url.QueryUnescape(s[pos+1:])
if err == nil {
return s[0:pos+1] + qy
}
}
return s
}
//
// -------------------- base encode --------------------
//
// base32 encoding with no padding
var (
B32Std = base32.StdEncoding.WithPadding(base32.NoPadding)
B32Hex = base32.HexEncoding.WithPadding(base32.NoPadding)
)
// B32Encode base32 encode
func B32Encode(str string) string {
return B32Std.EncodeToString([]byte(str))
}
// B32Decode base32 decode
func B32Decode(str string) string {
dec, _ := B32Std.DecodeString(str)
return string(dec)
}
// B64Std base64 encoding with no padding
var B64Std = base64.StdEncoding.WithPadding(base64.NoPadding)
// B64Encode base64 encode
func B64Encode(str string) string {
return B64Std.EncodeToString([]byte(str))
}
// B64EncodeBytes base64 encode
func B64EncodeBytes(src []byte) []byte {
buf := make([]byte, B64Std.EncodedLen(len(src)))
B64Std.Encode(buf, src)
return buf
}
// B64Decode base64 decode
func B64Decode(str string) string {
dec, _ := B64Std.DecodeString(str)
return string(dec)
}
// B64DecodeBytes base64 decode
func B64DecodeBytes(str []byte) []byte {
dbuf := make([]byte, B64Std.DecodedLen(len(str)))
n, _ := B64Std.Decode(dbuf, str)
return dbuf[:n]
}
+140
View File
@@ -0,0 +1,140 @@
package strutil
import (
"fmt"
"strings"
"github.com/gookit/goutil/mathutil"
)
// SimilarComparator definition
//
// links:
//
// https://github.com/mkideal/cli/blob/master/fuzzy.go
type SimilarComparator struct {
src, dst string
}
// NewComparator create
func NewComparator(src, dst string) *SimilarComparator {
return &SimilarComparator{src, dst}
}
// Similarity calc for two string.
//
// Usage:
//
// rate, ok := Similarity("hello", "he")
func Similarity(s, t string, rate float32) (float32, bool) {
return NewComparator(s, t).Similar(rate)
}
// Similar by minDifferRate
//
// Usage:
//
// c := NewComparator("hello", "he")
// rate, ok :c.Similar(0.3)
func (c *SimilarComparator) Similar(minDifferRate float32) (float32, bool) {
dist := c.editDistance([]byte(c.src), []byte(c.dst))
differRate := dist / float32(mathutil.Max(len(c.src), len(c.dst))+4)
return differRate, differRate >= minDifferRate
}
func (c *SimilarComparator) editDistance(s, t []byte) float32 {
var (
m = len(s)
n = len(t)
d = make([][]float32, m+1)
)
for i := 0; i < m+1; i++ {
d[i] = make([]float32, n+1)
d[i][0] = float32(i)
}
for j := 0; j < n+1; j++ {
d[0][j] = float32(j)
}
for j := 1; j < n+1; j++ {
for i := 1; i < m+1; i++ {
if s[i-1] == t[j-1] {
d[i][j] = d[i-1][j-1]
} else {
d[i][j] = mathutil.Min(d[i-1][j]+1, mathutil.Min(d[i][j-1]+1, d[i-1][j-1]+1))
}
}
}
return d[m][n]
}
// Builder struct
type Builder struct {
strings.Builder
}
// Write bytes and no error report
func (b *Builder) Write(p []byte) {
_, _ = b.Builder.Write(p)
}
// WriteRune and no error report
func (b *Builder) WriteRune(r rune) {
_, _ = b.Builder.WriteRune(r)
}
// WriteByteNE write byte and no error report
func (b *Builder) WriteByteNE(c byte) {
_ = b.WriteByte(c)
}
// WriteString to builder
func (b *Builder) WriteString(s string) {
_, _ = b.Builder.WriteString(s)
}
// Writef write string by fmt.Sprintf formatted
func (b *Builder) Writef(tpl string, vs ...any) {
_, _ = b.Builder.WriteString(fmt.Sprintf(tpl, vs...))
}
// Writeln write string with newline.
func (b *Builder) Writeln(s string) {
_, _ = b.Builder.WriteString(s)
_ = b.WriteByte('\n')
}
// WriteAny write any type value.
func (b *Builder) WriteAny(v any) {
_, _ = b.Builder.WriteString(QuietString(v))
}
// WriteAnys write any type values.
func (b *Builder) WriteAnys(vs ...any) {
for _, v := range vs {
_, _ = b.Builder.WriteString(QuietString(v))
}
}
// WriteMulti write multi byte at once.
func (b *Builder) WriteMulti(bs ...byte) {
for _, b2 := range bs {
_ = b.WriteByte(b2)
}
}
// WriteStrings write multi string at once.
func (b *Builder) WriteStrings(ss ...string) {
for _, s := range ss {
_, _ = b.Builder.WriteString(s)
}
}
// ResetGet return current string and reset builder
func (b *Builder) ResetGet() string {
s := b.String()
b.Reset()
return s
}
+72
View File
@@ -0,0 +1,72 @@
package strutil
import "strings"
/*************************************************************
* String filtering
*************************************************************/
// Trim string. if cutSet is empty, will trim SPACE.
func Trim(s string, cutSet ...string) string {
if ln := len(cutSet); ln > 0 && cutSet[0] != "" {
if ln == 1 {
return strings.Trim(s, cutSet[0])
}
return strings.Trim(s, strings.Join(cutSet, ""))
}
return strings.TrimSpace(s)
}
// Ltrim alias of TrimLeft
func Ltrim(s string, cutSet ...string) string { return TrimLeft(s, cutSet...) }
// LTrim alias of TrimLeft
func LTrim(s string, cutSet ...string) string { return TrimLeft(s, cutSet...) }
// TrimLeft char in the string. if cutSet is empty, will trim SPACE.
func TrimLeft(s string, cutSet ...string) string {
if ln := len(cutSet); ln > 0 && cutSet[0] != "" {
if ln == 1 {
return strings.TrimLeft(s, cutSet[0])
}
return strings.TrimLeft(s, strings.Join(cutSet, ""))
}
return strings.TrimLeft(s, " ")
}
// Rtrim alias of TrimRight
func Rtrim(s string, cutSet ...string) string { return TrimRight(s, cutSet...) }
// RTrim alias of TrimRight
func RTrim(s string, cutSet ...string) string { return TrimRight(s, cutSet...) }
// TrimRight char in the string. if cutSet is empty, will trim SPACE.
func TrimRight(s string, cutSet ...string) string {
if ln := len(cutSet); ln > 0 && cutSet[0] != "" {
if ln == 1 {
return strings.TrimRight(s, cutSet[0])
}
return strings.TrimRight(s, strings.Join(cutSet, ""))
}
return strings.TrimRight(s, " ")
}
// FilterEmail filter email, clear invalid chars.
func FilterEmail(s string) string {
s = strings.TrimSpace(s)
i := strings.LastIndex(s, "@")
if i == -1 {
return s
}
// According to rfc5321, "The local-part of a mailbox MUST BE treated as case-sensitive"
return s[0:i] + "@" + strings.ToLower(s[i+1:])
}
// func Filter(ss []string, fls ...comdef.StringMatchFunc) []string {
// }
+236
View File
@@ -0,0 +1,236 @@
package strutil
import (
"fmt"
"regexp"
"strings"
"unicode"
)
/*************************************************************
* change string case
*************************************************************/
// methods aliases
var (
UpWords = UpperWord
LoFirst = LowerFirst
UpFirst = UpperFirst
Snake = SnakeCase
)
// Title alias of the strings.ToTitle()
func Title(s string) string { return strings.ToTitle(s) }
// Lower alias of the strings.ToLower()
func Lower(s string) string { return strings.ToLower(s) }
// Lowercase alias of the strings.ToLower()
func Lowercase(s string) string { return strings.ToLower(s) }
// Upper alias of the strings.ToUpper()
func Upper(s string) string { return strings.ToUpper(s) }
// Uppercase alias of the strings.ToUpper()
func Uppercase(s string) string { return strings.ToUpper(s) }
// UpperWord Change the first character of each word to uppercase
func UpperWord(s string) string {
if len(s) == 0 {
return s
}
if len(s) == 1 {
return strings.ToUpper(s)
}
inWord := true
buf := make([]byte, 0, len(s))
i := 0
rs := []rune(s)
if RuneIsLower(rs[i]) {
buf = append(buf, []byte(string(unicode.ToUpper(rs[i])))...)
} else {
buf = append(buf, []byte(string(rs[i]))...)
}
for j := i + 1; j < len(rs); j++ {
if !RuneIsWord(rs[i]) && RuneIsWord(rs[j]) {
inWord = false
}
if RuneIsLower(rs[j]) && !inWord {
buf = append(buf, []byte(string(unicode.ToUpper(rs[j])))...)
inWord = true
} else {
buf = append(buf, []byte(string(rs[j]))...)
}
if RuneIsWord(rs[j]) {
inWord = true
}
i++
}
return string(buf)
}
// LowerFirst lower first char
func LowerFirst(s string) string {
if len(s) == 0 {
return s
}
rs := []rune(s)
f := rs[0]
if 'A' <= f && f <= 'Z' {
return string(unicode.ToLower(f)) + string(rs[1:])
}
return s
}
// UpperFirst upper first char
func UpperFirst(s string) string {
if len(s) == 0 {
return s
}
rs := []rune(s)
f := rs[0]
if 'a' <= f && f <= 'z' {
return string(unicode.ToUpper(f)) + string(rs[1:])
}
return s
}
// SnakeCase convert. eg "RangePrice" -> "range_price"
func SnakeCase(s string, sep ...string) string {
sepChar := "_"
if len(sep) > 0 {
sepChar = sep[0]
}
str := toSnakeReg.ReplaceAllStringFunc(s, func(s string) string {
return sepChar + LowerFirst(s)
})
return strings.TrimLeft(str, sepChar)
}
// Camel alias of the CamelCase
func Camel(s string, sep ...string) string { return CamelCase(s, sep...) }
// CamelCase convert string to camel case.
//
// Support:
//
// "range_price" -> "rangePrice"
// "range price" -> "rangePrice"
// "range-price" -> "rangePrice"
func CamelCase(s string, sep ...string) string {
sepChar := "_"
if len(sep) > 0 {
sepChar = sep[0]
}
// Not contains sep char
if !strings.Contains(s, sepChar) {
return s
}
// Get regexp instance
rgx, ok := toCamelRegs[sepChar]
if !ok {
rgx = regexp.MustCompile(regexp.QuoteMeta(sepChar) + "+[a-zA-Z]")
}
return rgx.ReplaceAllStringFunc(s, func(s string) string {
s = strings.TrimLeft(s, sepChar)
return UpperFirst(s)
})
}
//
// Indent format multi line text
// from package: github.com/kr/text
//
// Indent inserts prefix at the beginning of each non-empty line of s. The
// end-of-line marker is NL.
func Indent(s, prefix string) string {
return string(IndentBytes([]byte(s), []byte(prefix)))
}
// IndentBytes inserts prefix at the beginning of each non-empty line of b.
// The end-of-line marker is NL.
func IndentBytes(b, prefix []byte) []byte {
if len(b) == 0 {
return b
}
bol := true
res := make([]byte, 0, len(b)+len(prefix)*4)
for _, c := range b {
if bol && c != '\n' {
res = append(res, prefix...)
}
res = append(res, c)
bol = c == '\n'
}
return res
}
// Replaces replace multi strings
//
// pairs: {old1: new1, old2: new2, ...}
//
// Can also use:
//
// strings.NewReplacer("old1", "new1", "old2", "new2").Replace(str)
func Replaces(str string, pairs map[string]string) string {
return NewReplacer(pairs).Replace(str)
}
// ReplaceVars replaces simple variables in a string. format: {varName}
//
// Usage:
// strutil.ReplaceVars("{name}, age is {age}", map[string]string{
// "name": "Joe",
// "age": "18"
// })
func ReplaceVars(s string, vars map[string]string) string {
if !ContainsByte(s, '{') {
return s
}
// format var name to {name}
pairs := make(map[string]string)
for k, v := range vars {
vName := "{" + k + "}"
pairs[vName] = v
}
return NewReplacer(pairs).Replace(s)
}
// NewReplacer instance
func NewReplacer(pairs map[string]string) *strings.Replacer {
ss := make([]string, len(pairs)*2)
for old, newVal := range pairs {
ss = append(ss, old, newVal)
}
return strings.NewReplacer(ss...)
}
// WrapTag for given string.
func WrapTag(s, tag string) string {
if s == "" {
return s
}
return fmt.Sprintf("<%s>%s</%s>", tag, s, tag)
}
+291
View File
@@ -0,0 +1,291 @@
package strutil
import (
"fmt"
"hash/crc32"
"math/rand" // TODO use v2 on 1.22+
"os"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/gookit/goutil/x/basefn"
)
// global id:
//
// https://github.com/rs/xid
// https://github.com/satori/go.uuid
var (
DefMinInt = 1000
DefMaxInt = 9999
)
// MicroTimeID generate.
// - return like: 16074145697981929446(len: 20)
//
// Conv Base:
//
// mtId := MicroTimeID() // eg: 16935349145643425047 len: 20
// b16id := Base10Conv(mtId, 16) // eg: eb067252154a9d17 len: 16
// b32id := Base10Conv(mtId, 32) // eg: em1jia8akl78n len: 13
// b36id := Base10Conv(mtId, 36) // eg: 3ko088phiuoev len: 13
// b62id := Base10Conv(mtId, 62) // eg: kb24SKgsQ9V len: 11
func MicroTimeID() string { return MTimeBaseID(10) }
// MicroTimeHexID micro time HEX ID generate.
//
// return like: 643d4cec7db9e(len: 13)
func MicroTimeHexID() string { return MTimeHexID() }
// MTimeHexID micro time HEX ID generate.
//
// return like: 643d4cec7db9e(len: 13)
func MTimeHexID() string { return MTimeBaseID(16) }
// MTimeBase36 micro time BASE36 id generate.
func MTimeBase36() string { return MTimeBaseID(36) }
// MTimeBaseID micro time BASE id generate. toBase: 2-36
//
// Examples:
// - toBase=16: 643d4cec7db9e(len: 13)
// - toBase=36: hd312z9ka2(len: 10)
func MTimeBaseID(toBase int) string {
// eg: 1763431181849557
ms := time.Now().UnixMicro()
// rand 1000 - 9999
// ri := mathutil.RandomInt(DefMinInt, DefMaxInt)
ri := 1000 + rand.Int63n(8999)
if toBase > 36 {
return BaseConvInt(uint64(ms)+uint64(ri), toBase)
}
return strconv.FormatInt(ms+ri, toBase)
}
// DatetimeNo generate. can use for order-no.
//
// - No prefix, return like: 2023041410484904074285478388(len: 28)
// - With prefix, return like: prefix2023041410484904074285478388(len: 28 + len(prefix))
func DatetimeNo(prefix string) string { return DateSN(prefix) }
// DateSN generate date serial number. PREFIX + yyyyMMddHHmmss + ext(微秒+随机数)
func DateSN(prefix string) string {
nt := time.Now()
pl := len(prefix)
bs := make([]byte, 0, 28+pl)
if pl > 0 {
bs = append(bs, prefix...)
}
// micro datetime
bs = nt.AppendFormat(bs, "20060102150405.000000")
bs[14+pl] = '0'
// host
name, err := os.Hostname()
if err != nil {
name = "default"
}
c32 := crc32.ChecksumIEEE([]byte(name)) // eg: 4006367001
bs = strconv.AppendUint(bs, uint64(c32%99), 10)
// rand 1000 - 9999
// rs := rand.New(rand.NewSource(nt.UnixNano()))
bs = strconv.AppendInt(bs, 1000+rand.Int63n(8999), 10)
return string(bs)
}
// DateSNOpt 基于时间生成唯一编号
type DateSNOpt struct {
Layout string // time layout
// RandMax int // rand max
DateLen int // 时间格式长度,后面部分将会进行进制转换 默认 8(yyyyMMdd)
ConvBase int // DateLen 之后的转换 base 2-64. default 36
// EnableSeq bool // 需要高并发生成时可以启用自增序号。默认不启用
SeqMaxVal int // 自增序号最大值,之后后自动重置
globalSeq int64 // 自增,确保同一时刻生成的编号不重复. EnableSeq=true 时启用
}
// default setting: {时间年到秒14位}
var defOpt = NewDateSNOpt()
// ConfigSNOpt config default date sn option
func ConfigSNOpt(fn func(opt *DateSNOpt)) {
fn(defOpt)
}
// NewDateSNOpt create a new DateSNOpt instance.
func NewDateSNOpt() *DateSNOpt {
return &DateSNOpt{
Layout: "20060102150405.000000",
// RandMax: 8999,
DateLen: 8,
ConvBase: 36,
// EnableSeq: true,
SeqMaxVal: 8999,
}
}
// prepare for generate
func (do *DateSNOpt) prepare() {
if do.DateLen <= 0 {
do.DateLen = 8 // default 8 for yyyyMMdd
}
if do.ConvBase <= 0 {
do.ConvBase = 36
}
if do.Layout == "" {
do.Layout = "20060102150405.000000"
}
// get sequence max value (default 9999)
if do.SeqMaxVal <= 0 {
do.SeqMaxVal = 8999
}
}
func (do *DateSNOpt) getSeqValue() int64 {
// use atomic sequence for guaranteed uniqueness (even without EnableSeq)
// this ensures no collisions in tight loops
seq := atomic.AddInt64(&do.globalSeq, 1)
// auto reset when seq exceeds SeqMaxVal (thread-safe using CAS)
if seq > int64(do.SeqMaxVal) {
// try to reset to 1, other goroutines may have already done it
atomic.CompareAndSwapInt64(&do.globalSeq, seq, 1)
seq = seq % int64(do.SeqMaxVal)
if seq == 0 {
seq = 1
}
}
return 1000 + seq
}
// GenSN generate date serial number.
func (do *DateSNOpt) GenSN(prefix string) string {
pl := len(prefix)
bs := make([]byte, 0, 22+pl)
if pl > 0 {
bs = append(bs, prefix...)
}
do.prepare()
// get time and format
nt := time.Now()
bs = nt.AppendFormat(bs, do.Layout)
// remove the dot separator if exists
dotIdx := -1
for i := pl; i < len(bs); i++ {
if bs[i] == '.' {
dotIdx = i
break
}
}
if dotIdx > 0 {
bs = append(bs[:dotIdx], bs[dotIdx+1:]...)
}
// determine date length (default 8 for yyyyMMdd)
idx := do.DateLen + pl
// high concurrency mode: sequence is the main differentiator
extBs := strconv.AppendInt(bs[idx:], do.getSeqValue(), 10)
if extBs[0] == '0' {
extBs[0] = '1'
}
extInt := SafeUint(string(extBs))
// convert extension to target base
bs = append(bs[:idx], BaseConvInt(extInt, do.ConvBase)...)
return string(bs)
}
// 确保同一时刻生成的编号不重复 max: 89999
var globalSeqSnV2 int64 = 0
func getSeqValue() int64 {
// use atomic sequence for guaranteed uniqueness (even without EnableSeq)
// this ensures no collisions in tight loops
seq := atomic.AddInt64(&globalSeqSnV2, 1)
// auto reset when seq exceeds SeqMaxVal (thread-safe using CAS)
if seq > 89999 {
// try to reset to 1, other goroutines may have already done it
atomic.CompareAndSwapInt64(&globalSeqSnV2, seq, 1)
seq = seq % 89999
if seq == 0 {
seq = 1
}
}
return seq
}
// DateSNv2 generate date serial number.
// - 2 < extBase <= 36
// - return: PREFIX + yyyyMMddHHmmss + extBase(6bit micro + 5bit random number)
//
// Example:
// - prefix=P, extBase=16, return: P2023091414361354b4490(len=22)
// - prefix=P, extBase=36, return: P202309141436131gw3jg(len=21)
func DateSNv2(prefix string, extBase ...int) string {
pl := len(prefix)
bs := make([]byte, 0, 22+pl)
if pl > 0 {
bs = append(bs, prefix...)
}
// micro datetime
nt := time.Now()
bs = nt.AppendFormat(bs, "20060102150405.000000")
// 6bit micro + 5bit rand 10000 - 99999
// ext := strconv.AppendInt(bs[16+pl:], 10000+rand.Int63n(89999), 10)
ext := strconv.AppendInt(bs[16+pl:], 10000+getSeqValue(), 10)
// prefix + yyyyMMddHHmmss + ext(convert to base)
base := basefn.FirstOr(extBase, 36)
bs = append(bs[:14+pl], strconv.FormatInt(SafeInt64(string(ext)), base)...)
return string(bs)
}
var (
// key is dateLen + extBase
dsoMap = make(map[string]*DateSNOpt)
dsoMutex sync.RWMutex
)
// DateSNv3 generate date serial number.
// - 2 < extBase <= 64
// - return: PREFIX + DATETIME(yyyyMMddHHmmss).dateLen + extBase(DATETIME.after+6bit micro + 5bit random number)
// - dateLen: 为 DATETIME(yyyyMMddHHmmss) 保留的长度,默认为 8(yyyyMMdd) 后面的给 extBase 使用
//
// Example:
// - prefix=P, dateLen=8, extBase=16, return: P202511139vs99gbifnj len: 20
// - prefix=P, dateLen=6, extBase=36, return: P2025119yn52qhefati len: 19
// - prefix=P, dateLen=6, extBase=48, return: P202511k9ksgD1fe6x len: 18
// - prefix=P, dateLen=4, extBase=62, return: P2025aZl8N0y58M7 len: 16
func DateSNv3(prefix string, dateLen int, extBase ...int) string {
baseVal := basefn.FirstOr(extBase, 36)
cacheKey := fmt.Sprintf("%d%d", dateLen, baseVal)
dsoMutex.RLock()
dso, ok := dsoMap[cacheKey]
dsoMutex.RUnlock()
if !ok {
dsoMutex.Lock()
// double check,防止在加锁期间其他 goroutine 已经创建
if dso, ok = dsoMap[cacheKey]; !ok {
dso = NewDateSNOpt()
// dso.EnableSeq = true
dso.DateLen = dateLen
dso.ConvBase = baseVal
dsoMap[cacheKey] = dso
}
dsoMutex.Unlock()
}
return dso.GenSN(prefix)
}
+121
View File
@@ -0,0 +1,121 @@
package strutil
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"math/big"
"strings"
"github.com/gookit/goutil/byteutil"
)
// Md5 Generate a 32-bit md5 string
func Md5(src any) string { return string(Md5Bytes(src)) }
// MD5 Generate a 32-bit md5 string
func MD5(src any) string { return Md5(src) }
// GenMd5 Generate a 32-bit md5 string
func GenMd5(src any) string { return Md5(src) }
// Md5Simple md5 加密原始二进制的每个byte转为 base62,缩短长度(16)
func Md5Simple(src any) string {
md5Bytes := byteutil.Md5Sum(src)
b62str := new(strings.Builder)
b62str.Grow(16)
// 直接将每个 byte 转为 base62 数字然后拼接
for _, b := range md5Bytes {
b62str.WriteByte(Base62Chars[b%0x3E])
}
return b62str.String()
}
// Md5Base62 md5 加密原始二进制的转为 base62 字符串,缩短长度(21~22)
func Md5Base62(src any) string {
md5Bytes := byteutil.Md5Sum(src)
// Step 2: 将字节数组转为 big.Int
number := new(big.Int)
number.SetBytes(md5Bytes)
// Step 3: 转换为 Base62 字符串
base62 := new(strings.Builder)
// base62.Grow(22)
for number.Sign() > 0 {
remainder := new(big.Int)
number.DivMod(number, big.NewInt(62), remainder)
base62.WriteByte(Base62Chars[remainder.Int64()])
}
// 反转字符串以得到正确的顺序
runes := []rune(base62.String())
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
// Md5Bytes Generate a 32-bit md5 bytes
func Md5Bytes(src any) []byte { return byteutil.Md5(src) }
// ShortMd5 Generate a 16-bit md5 string. remove the first 8 and last 8 bytes from 32-bit md5 string.
func ShortMd5(src any) string { return string(byteutil.ShortMd5(src)) }
//
// ----------------------- Simple UUID -----------------------------
//
// UUIDv4 Generate a simple UUIDv4 string
func UUIDv4() (string, error) {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("generate UUID fail: %w", err)
}
// 设置版本(4)和变体
b[6] = (b[6] & 0x0f) | 0x40 // Version 4
b[8] = (b[8] & 0x3f) | 0x80 // Variant 10
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]), nil
}
// ShortUUID Generate a short UUID (8 characters)
func ShortUUID() string {
b := make([]byte, 4)
if _, err := rand.Read(b); err != nil {
return fmt.Sprintf("%04x", b)
}
return fmt.Sprintf("%08x", b)
}
//
// ----------------------- hash password -----------------------------
//
// HashPasswd for quick hash an input password string
func HashPasswd(pwd, key string) string {
hm := hmac.New(sha256.New, []byte(key))
hm.Write([]byte(pwd))
return hex.EncodeToString(hm.Sum(nil))
}
// VerifyPasswd for quick verify input password is valid
//
// - pwdMAC from db or config, generated by HashPasswd()
func VerifyPasswd(pwdMAC, pwd, key string) bool {
decBts, err := hex.DecodeString(pwdMAC)
if err != nil {
return false
}
hm := hmac.New(sha256.New, []byte(key))
hm.Write([]byte(pwd))
return hmac.Equal(decBts, hm.Sum(nil))
}
+221
View File
@@ -0,0 +1,221 @@
package strutil
import (
"strings"
"github.com/gookit/goutil/comdef"
)
// PosFlag type
type PosFlag = comdef.Position
// Position for padding/resize string
const (
PosLeft PosFlag = iota
PosMiddle
PosRight
PosAuto
)
/*************************************************************
* String padding operation
*************************************************************/
// Padding Fill the string to the specified length.
//
// params:
// - s: 原始字符串
// - pad: 用于填充的字符或字符串
// - length: 目标长度
// - padPos: 填充位置标志(左填充或右填充)
func Padding(s, pad string, length int, padPos PosFlag) string {
return padding(s, pad, len(s), length, padPos)
}
// Utf8Padding Fill the string to the specified length. use utf8 width.
func Utf8Padding(s, pad string, wantLen int, padPos PosFlag) string {
return padding(s, pad, Utf8Width(s), wantLen, padPos)
}
func padding(s, pad string, sLen, wantLen int, padPos PosFlag) string {
diff := sLen - wantLen
if diff >= 0 { // do not need padding.
return s
}
// pad space.
if len(s) == sLen && (pad == "" || pad == " ") {
// Sprintf: 是按字数来填充的,不管中英文都是一个字符 - 有问题
if padPos == PosRight { // to right
return s + strings.Repeat(" ", -diff)
}
return strings.Repeat(" ", -diff) + s
}
// other character.
if padPos == PosRight { // to right
return s + Repeat(pad, -diff)
}
return Repeat(pad, -diff) + s
}
// PadLeft a string.
func PadLeft(s, pad string, length int) string {
return Padding(s, pad, length, PosLeft)
}
// PadRight a string.
func PadRight(s, pad string, length int) string {
return Padding(s, pad, length, PosRight)
}
// PadChars padding a rune/byte to want length and with position flag
func PadChars[T byte | rune](cs []T, pad T, length int, pos PosFlag) []T {
ln := len(cs)
if ln >= length {
ns := make([]T, length)
copy(ns, cs[:length])
return ns
}
idx := length - ln
ns := make([]T, length)
if pos == PosRight {
copy(ns, cs)
for i := ln; i < length; i++ {
ns[i] = pad
}
return ns
}
// to left
for i := 0; i < idx; i++ {
ns[i] = pad
}
copy(ns[idx:], cs)
return ns
}
// PadBytes padding a byte to want length and with position flag
func PadBytes(bs []byte, pad byte, length int, pos PosFlag) []byte {
return PadChars(bs, pad, length, pos)
}
// PadBytesLeft a byte to want length
func PadBytesLeft(bs []byte, pad byte, length int) []byte {
return PadChars(bs, pad, length, PosLeft)
}
// PadBytesRight a byte to want length
func PadBytesRight(bs []byte, pad byte, length int) []byte {
return PadChars(bs, pad, length, PosRight)
}
// PadRunes padding a rune to want length and with position flag
func PadRunes(rs []rune, pad rune, length int, pos PosFlag) []rune {
return PadChars(rs, pad, length, pos)
}
// PadRunesLeft a rune to want length
func PadRunesLeft(rs []rune, pad rune, length int) []rune {
return PadChars(rs, pad, length, PosLeft)
}
// PadRunesRight a rune to want length
func PadRunesRight(rs []rune, pad rune, length int) []rune {
return PadChars(rs, pad, length, PosRight)
}
// Align a string by given length and align settings. alias of Resize
func Align(s string, length int, align comdef.Align) string {
return resize(s, len(s), length, align, false)
}
// Utf8Align a string by given length and align settings. alias of Utf8Resize
func Utf8Align(s string, length int, align comdef.Align) string {
return resize(s, Utf8Width(s), length, align, false)
}
// Resize a string by given length and align settings. use padding space.
// If len(s) > wantLen, will truncate it.
func Resize(s string, length int, align comdef.Align) string {
return resize(s, len(s), length, align, true)
}
// Utf8Resize a string by given length and align settings. use padding space.
// If width(s) > wantLen, will truncate it.
func Utf8Resize(s string, length int, align comdef.Align) string {
return resize(s, Utf8Width(s), length, align, true)
}
// resize a string by given length and align settings. use padding space.
func resize(s string, sLen, wantLen int, align comdef.Align, cutOverflow bool) string {
diff := sLen - wantLen
if diff >= 0 { // do not need padding.
// cutOverflow: truncate on sLen > wantLen
if cutOverflow && sLen > wantLen {
if len(s) == sLen {
return s[:wantLen]
}
return utf8Truncate(s, sLen, wantLen, "")
}
return s
}
if align == comdef.Center {
padLn := (wantLen - sLen) / 2
if diff = wantLen - padLn*2; diff > 0 {
s += " "
}
padStr := string(RepeatBytes(' ', padLn))
return padStr + s + padStr
}
padStr := string(RepeatBytes(' ', wantLen-sLen))
// tip: 左对齐 - 使用空白填充右边
if align == comdef.Left || align == comdef.PosAuto {
return s + padStr
}
return padStr + s
}
/*************************************************************
* String repeat operation
*************************************************************/
// Repeat a string by given times.
func Repeat(s string, times int) string {
if times < 1 {
return ""
}
if times == 1 {
return s
}
var sb strings.Builder
sb.Grow(len(s) * times)
for i := 0; i < times; i++ {
sb.WriteString(s)
}
return sb.String()
}
// RepeatRune repeat a rune char.
func RepeatRune(char rune, times int) []rune { return RepeatChars(char, times) }
// RepeatBytes repeat a byte char.
func RepeatBytes(char byte, times int) []byte { return RepeatChars(char, times) }
// RepeatChars repeat a byte char.
func RepeatChars[T byte | rune](char T, times int) []T {
if times <= 0 {
return make([]T, 0)
}
chars := make([]T, times)
for i := 0; i < times; i++ {
chars[i] = char
}
return chars
}
+239
View File
@@ -0,0 +1,239 @@
package strutil
import (
"errors"
"regexp"
"strconv"
"strings"
"time"
"unicode"
"github.com/gookit/goutil/byteutil"
)
var regNumVersion = regexp.MustCompile(`[0-9][\d.]+([_-]\d+)?`)
// NumVersion parse input string, get valid number version. eg: go-1.22.3 -> 1.22.3
func NumVersion(s string) string {
return regNumVersion.FindString(s)
}
// MustToTime convert date string to time.Time
func MustToTime(s string, layouts ...string) time.Time {
t, err := ToTime(s, layouts...)
if err != nil {
panic(err)
}
return t
}
// auto match uses some common layouts.
// key is layout length.
var layoutMap = map[int][]string{
6: {"200601", "060102", time.Kitchen},
8: {"20060102", "06-01-02"},
10: {"2006-01-02"},
13: {"2006-01-02 15"},
15: {time.Stamp},
16: {"2006-01-02 15:04"},
19: {"2006-01-02 15:04:05", time.RFC822, time.StampMilli},
20: {"2006-01-02 15:04:05Z"},
21: {time.RFC822Z},
22: {time.StampMicro},
23: {"2006-01-02 15:04:05.000", "2006-01-02 15:04:05.999"},
24: {time.ANSIC},
25: {time.RFC3339, time.StampNano},
// time.Layout}, // must go >= 1.19
26: {"2006-01-02 15:04:05.000000"},
28: {time.UnixDate},
29: {time.RFC1123, "2006-01-02 15:04:05.000000000"},
30: {time.RFC850},
31: {time.RFC1123Z},
35: {time.RFC3339Nano},
}
// ToTime convert date string to time.Time
//
// NOTE: always use local timezone.
func ToTime(s string, layouts ...string) (t time.Time, err error) {
// custom layout
if len(layouts) > 0 {
if len(layouts[0]) > 0 {
return time.ParseInLocation(layouts[0], s, time.Local)
}
err = ErrDateLayout
return
}
// auto match use some commonly layouts.
strLn := len(s)
maybeLayouts, ok := layoutMap[strLn]
if !ok {
err = ErrInvalidParam
return
}
var hasAlphaT bool
if pos := strings.IndexByte(s, 'T'); pos > 0 && pos < 12 {
hasAlphaT = true
}
hasSlashR := strings.IndexByte(s, '/') > 0
for _, layout := range maybeLayouts {
// date string has "T". eg: "2006-01-02T15:04:05"
if hasAlphaT {
layout = strings.Replace(layout, " ", "T", 1)
}
// date string has "/". eg: "2006/01/02 15:04:05"
if hasSlashR {
layout = strings.Replace(layout, "-", "/", -1)
}
t, err = time.ParseInLocation(layout, s, time.Local)
if err == nil {
return
}
}
// t, err = time.ParseInLocation(layout, s, time.Local)
return
}
// ParseSizeOpt parse size expression options
type ParseSizeOpt struct {
// OneAsMax if only one size value, use it as max size. default is false
OneAsMax bool
// SepChar is the separator char for time range string. default is '~'
SepChar byte
// KeywordFn is the function for parse keyword time string.
KeywordFn func(string) (min, max uint64, err error)
}
func ensureOpt(opt *ParseSizeOpt) *ParseSizeOpt {
if opt == nil {
opt = &ParseSizeOpt{SepChar: '~'}
} else {
if opt.SepChar == 0 {
opt.SepChar = '~'
}
}
return opt
}
// ErrInvalidSizeExpr invalid size expression error
var ErrInvalidSizeExpr = errors.New("invalid size expr")
// ParseSizeRange parse range size expression to min and max size.
//
// Expression format:
//
// "1KB~2MB" => 1KB to 2MB
// "-1KB" => <1KB
// "~1MB" => <1MB
// "< 1KB" => <1KB
// "1KB" => >1KB
// "1KB~" => >1KB
// ">1KB" => >1KB
// "+1KB" => >1KB
func ParseSizeRange(expr string, opt *ParseSizeOpt) (min, max uint64, err error) {
opt = ensureOpt(opt)
expr = strings.TrimSpace(expr)
if expr == "" {
err = ErrInvalidSizeExpr
return
}
// parse size range. eg: "1KB~2MB"
if strings.IndexByte(expr, '~') > -1 {
s1, s2 := TrimCut(expr, "~")
if s1 != "" {
min, err = ToByteSize(s1)
if err != nil {
return
}
}
if s2 != "" {
max, err = ToByteSize(s2)
}
return
}
// parse single size. eg: "1KB"
if byteutil.IsNumChar(expr[0]) {
min, err = ToByteSize(expr)
if err != nil {
return
}
if opt.OneAsMax {
max = min
}
return
}
// parse with prefix. eg: "<1KB", ">= 1KB", "-1KB", "+1KB"
switch expr[0] {
case '<', '-':
max, err = ToByteSize(strings.Trim(expr[1:], " ="))
case '>', '+':
min, err = ToByteSize(strings.Trim(expr[1:], " ="))
default:
// parse keyword. eg: "small", "large"
if opt.KeywordFn != nil {
min, max, err = opt.KeywordFn(expr)
} else {
err = ErrInvalidSizeExpr
}
}
return
}
// SafeByteSize converts size string like 1GB/1g or 12mb/12M into an unsigned integer number of bytes
func SafeByteSize(sizeStr string) uint64 {
size, _ := ToByteSize(sizeStr)
return size
}
// ToByteSize converts size string like 1GB/1g or 12mb/12M into an unsigned integer number of bytes
func ToByteSize(sizeStr string) (uint64, error) {
sizeStr = strings.TrimSpace(sizeStr)
lastPos := len(sizeStr) - 1
if lastPos < 0 {
return 0, nil
}
if sizeStr[lastPos] == 'b' || sizeStr[lastPos] == 'B' {
// last second char is k,m,g,t
lastSec := sizeStr[lastPos-1]
if lastSec > 'A' {
lastPos--
}
} else if IsNumChar(sizeStr[lastPos]) { // not unit suffix. eg: 346
return strconv.ParseUint(sizeStr, 10, 32)
}
multiplier := float64(1)
switch unicode.ToLower(rune(sizeStr[lastPos])) {
case 'k':
multiplier = 1 << 10
case 'm':
multiplier = 1 << 20
case 'g':
multiplier = 1 << 30
case 't':
multiplier = 1 << 40
case 'p':
multiplier = 1 << 50
default: // b
multiplier = 1
}
sizeNum := strings.TrimSpace(sizeStr[:lastPos])
size, err := strconv.ParseFloat(sizeNum, 64)
if err != nil {
return 0, err
}
return uint64(size * multiplier), nil
}
+60
View File
@@ -0,0 +1,60 @@
package strutil
import (
"github.com/gookit/goutil/byteutil"
"github.com/gookit/goutil/x/encodes"
)
// some constant string chars
const (
Numbers = "0123456789"
HexChars = "0123456789abcdef" // base16
AlphaBet = "abcdefghijklmnopqrstuvwxyz"
AlphaBet1 = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz"
// AlphaNum chars, can use for base36 encode
AlphaNum = "abcdefghijklmnopqrstuvwxyz0123456789"
// AlphaNum2 chars, can use for base62 encode
AlphaNum2 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
AlphaNum3 = "0123456789AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz"
)
// RandomChars generate give length random chars at `a-z`
func RandomChars(ln int) string {
return buildRandomString(AlphaBet, ln)
}
// RandomCharsV2 generate give length random chars in `0-9a-z`
func RandomCharsV2(ln int) string {
return buildRandomString(AlphaNum, ln)
}
// RandomCharsV3 generate give length random chars in `0-9a-zA-Z`
func RandomCharsV3(ln int) string {
return buildRandomString(AlphaNum2, ln)
}
// RandWithTpl generate random string with give template
func RandWithTpl(n int, letters string) string {
if len(letters) == 0 {
letters = AlphaNum2
}
return buildRandomString(letters, n)
}
// RandomString generate.
//
// Example:
//
// // this will give us a 44 byte, base64 encoded output
// token, err := RandomString(16) // eg: "I7S4yFZddRMxQoudLZZ-eg"
func RandomString(length int) (string, error) {
b, err := RandomBytes(length)
return encodes.B64URL.EncodeToString(b), err
}
// RandomBytes generate
func RandomBytes(length int) ([]byte, error) {
return byteutil.Random(length)
}
+27
View File
@@ -0,0 +1,27 @@
//go:build !windows
package strutil
import (
"math/rand"
"time"
)
var rn = newRand()
func newRand() *rand.Rand {
return rand.New(rand.NewSource(time.Now().UnixNano()))
}
// buildRandomString 生成随机字符串
func buildRandomString(letters string, length int) string {
// rn := newRand()
cs := make([]byte, length)
lettersN := len(letters)
for i := 0; i < length; i++ {
cs[i] = letters[rn.Intn(lettersN)]
}
return Byte2str(cs)
}
+85
View File
@@ -0,0 +1,85 @@
//go:build windows
package strutil
import (
"math"
"math/rand"
"time"
"unsafe"
)
const MaximumCapacity = math.MaxInt>>1 + 1
var rn = rand.NewSource(time.Now().UnixNano())
// nearestPowerOfTwo 返回一个大于等于cap的最近的2的整数次幂,参考java8的hashmap的tableSizeFor函数
// - cap 输入参数
//
// 返回一个大于等于cap的最近的2的整数次幂
func nearestPowerOfTwo(cap int) int {
n := cap - 1
n |= n >> 1
n |= n >> 2
n |= n >> 4
n |= n >> 8
n |= n >> 16
if n < 0 {
return 1
}
if n >= MaximumCapacity {
return MaximumCapacity
}
return n + 1
}
// buildRandomString 生成随机字符串
// - letters 字符串模板
// - length 生成长度
//
// 返回一个指定长度的随机字符串
func buildRandomString(letters string, length int) string {
// 仿照strings.Builder
// 创建一个长度为 length 的字节切片
bytes := make([]byte, length)
strLength := len(letters)
if strLength <= 0 {
return ""
}
if strLength == 1 {
for i := 0; i < length; i++ {
bytes[i] = letters[0]
}
return *(*string)(unsafe.Pointer(&bytes))
}
// letters的字符需要使用多少个比特位数才能表示完
// letterIdBits := int(math.Ceil(math.Log2(strLength))),下面比上面的代码快
letterIdBits := int(math.Log2(float64(nearestPowerOfTwo(strLength))))
// 最大的字母id掩码
var letterIdMask int64 = 1<<letterIdBits - 1
// 可用次数的最大值
letterIdMax := 63 / letterIdBits
// UnixNano: 1607400451937462000
// 循环生成随机字符串
for i, cache, remain := length-1, rn.Int63(), letterIdMax; i >= 0; {
// 检查随机数生成器是否用尽所有随机数
if remain == 0 {
cache, remain = rn.Int63(), letterIdMax
}
// 从可用字符的字符串中随机选择一个字符
if idx := int(cache & letterIdMask); idx < strLength {
bytes[i] = letters[idx]
i--
}
// 右移比特位数,为下次选择字符做准备
cache >>= letterIdBits
remain--
}
// 仿照strings.Builder用unsafe包返回一个字符串,避免拷贝
// 将字节切片转换为字符串并返回
return *(*string)(unsafe.Pointer(&bytes))
}
+257
View File
@@ -0,0 +1,257 @@
package strutil
import (
"strings"
"unicode"
"unicode/utf8"
"golang.org/x/text/width"
)
// RuneIsWord char: a-zA-Z
func RuneIsWord(c rune) bool {
return RuneIsLower(c) || RuneIsUpper(c)
}
// RuneIsLower char
func RuneIsLower(c rune) bool {
return 'a' <= c && c <= 'z'
}
// RuneIsUpper char
func RuneIsUpper(c rune) bool {
return 'A' <= c && c <= 'Z'
}
// RunePos alias of the strings.IndexRune
func RunePos(s string, ru rune) int { return strings.IndexRune(s, ru) }
// IsSpaceRune returns true if the given rune is a space, otherwise false.
func IsSpaceRune(r rune) bool {
return r <= 256 && IsSpace(byte(r)) || unicode.IsSpace(r)
}
// Utf8Len count rune of the string.
//
// Examples:
//
// str := "hi,你好"
//
// len(str) // 9
// strutil.RunesWidth(str) // 7 一个中文字占两个字符
// RuneCount(str) = Utf8Len(s) // 5 按字算
func Utf8Len(s string) int { return utf8.RuneCountInString(s) }
// Utf8len count rune of the string.
func Utf8len(s string) int { return utf8.RuneCountInString(s) }
// RuneCount of the string.
//
// Examples:
//
// str := "hi,你好"
//
// len(str) // 9
// strutil.RunesWidth(str) // 7 一个中文字占两个字符
// RuneCount(str) = utf8.RuneCountInString(s) // 5 按字算
func RuneCount(s string) int { return len([]rune(s)) }
// RuneWidth of the rune.
//
// Example:
//
// RuneWidth('你') // 2
// RuneWidth('a') // 1
// RuneWidth('\n') // 0
func RuneWidth(r rune) int {
p := width.LookupRune(r)
k := p.Kind()
// eg: "\n"
if k == width.Neutral {
return 0
}
if k == width.EastAsianFullwidth || k == width.EastAsianWide || k == width.EastAsianAmbiguous {
return 2
}
return 1
}
// TextWidth utf8 string width. alias of RunesWidth()
func TextWidth(s string) int { return Utf8Width(s) }
// Utf8Width utf8 string width. alias of RunesWidth
func Utf8Width(s string) int { return RunesWidth([]rune(s)) }
// RunesWidth utf8 runes string width.
//
// Examples:
//
// str := "hi,你好"
//
// len(str) // 9
// strutil.RunesWidth(str) // 7 一个中文字占两个字符
// len([]rune(str)) = utf8.RuneCountInString(s) // 5 按字算
func RunesWidth(rs []rune) (w int) {
if len(rs) == 0 {
return
}
for _, runeVal := range rs {
w += RuneWidth(runeVal)
}
return w
}
// Truncate alias of the Utf8Truncate()
func Truncate(s string, w int, tail string) string { return Utf8Truncate(s, w, tail) }
// TextTruncate alias of the Utf8Truncate()
func TextTruncate(s string, w int, tail string) string { return Utf8Truncate(s, w, tail) }
// Utf8Truncate a string with given width.
func Utf8Truncate(s string, w int, tail string) string { return utf8Truncate(s, Utf8Width(s), w, tail) }
// utf8Truncate a string with given width.
func utf8Truncate(s string, sw, w int, tail string) string {
if sw <= w {
return s
}
i := 0
r := []rune(s)
w -= TextWidth(tail)
tmpW := 0
for ; i < len(r); i++ {
cw := RuneWidth(r[i])
if tmpW+cw > w {
break
}
tmpW += cw
}
return string(r[0:i]) + tail
}
// Chunk split string to chunks by size.
// func Chunk[T ~string](s T, size int) []T {
// }
// TextSplit alias of the Utf8Split()
func TextSplit(s string, w int) []string { return Utf8Split(s, w) }
// Utf8Split split a string by width.
func Utf8Split(s string, w int) []string {
sw := Utf8Width(s)
if sw <= w {
return []string{s}
}
tmpW := 0
tmpS := ""
ss := make([]string, 0, sw/w+1)
for _, r := range s {
rw := RuneWidth(r)
if tmpW+rw == w {
tmpS += string(r)
ss = append(ss, tmpS)
tmpW, tmpS = 0, "" // reset
continue
}
if tmpW+rw > w {
ss = append(ss, tmpS)
// append to next line.
tmpW, tmpS = rw, string(r)
continue
}
tmpW += rw
tmpS += string(r)
}
if tmpW > 0 {
ss = append(ss, tmpS)
}
return ss
}
// TextWrap a string by "\n". alias of the WidthWrap()
func TextWrap(s string, w int) string { return WidthWrap(s, w) }
// WidthWrap a string by "\n"
//
// Example:
// s := "hello 你好, world 世界"
// s1 := strutil.TextWrap(s, 6) // "hello \n你好, \nworld \n世界"
func WidthWrap(s string, w int) string {
tmpW := 0
out := ""
for _, r := range s {
cw := RuneWidth(r)
if r == '\n' {
out += string(r)
tmpW = 0
continue
}
if tmpW+cw > w {
out += "\n"
tmpW = 0
out += string(r)
tmpW += cw
continue
}
out += string(r)
tmpW += cw
}
return out
}
// WordWrap text string and limit width.
func WordWrap(s string, w int) string {
tmpW := 0
out := ""
for _, sub := range strings.Split(s, " ") {
cw := TextWidth(sub)
if tmpW+cw > w {
if tmpW != 0 {
out += "\n"
}
tmpW = 0
out += sub
tmpW += cw
continue
}
out += sub
tmpW += cw
}
return out
}
// Runes data slice
type Runes []rune
// Padding a rune to want length and with position
func (rs Runes) Padding(pad rune, length int, pos PosFlag) []rune {
return PadChars(rs, pad, length, pos)
}
// PadLeft a rune to want length
func (rs Runes) PadLeft(pad rune, length int) []rune {
return rs.Padding(pad, length, PosLeft)
}
// PadRight a rune to want length
func (rs Runes) PadRight(pad rune, length int) []rune {
return rs.Padding(pad, length, PosRight)
}
+200
View File
@@ -0,0 +1,200 @@
package strutil
import (
"regexp"
"strings"
)
// BeforeFirst get substring before first sep.
func BeforeFirst(s, sep string) string {
if i := strings.Index(s, sep); i >= 0 {
return s[:i]
}
return s
}
// AfterFirst get substring after first sep.
func AfterFirst(s, sep string) string {
if i := strings.Index(s, sep); i >= 0 {
return s[i+len(sep):]
}
return ""
}
// BeforeLast get substring before last sep.
func BeforeLast(s, sep string) string {
if i := strings.LastIndex(s, sep); i >= 0 {
return s[:i]
}
return s
}
// AfterLast get substring after last sep.
func AfterLast(s, sep string) string {
if i := strings.LastIndex(s, sep); i >= 0 {
return s[i+len(sep):]
}
return ""
}
/*************************************************************
* String split operation
*************************************************************/
// Cut alias of the strings.Cut
func Cut(s, sep string) (before string, after string, found bool) {
if i := strings.Index(s, sep); i >= 0 {
return s[:i], s[i+len(sep):], true
}
return s, "", false
}
// QuietCut always returns two substring.
func QuietCut(s, sep string) (before string, after string) {
before, after, _ = Cut(s, sep)
return
}
// MustCut always returns two substring.
func MustCut(s, sep string) (before string, after string) {
var ok bool
before, after, ok = Cut(s, sep)
if !ok {
panic("cannot split input string to two nodes")
}
return
}
// TrimCut always returns two substring and trim space for items.
func TrimCut(s, sep string) (string, string) {
before, after, _ := Cut(s, sep)
return strings.TrimSpace(before), strings.TrimSpace(after)
}
// SplitKV split string to key and value.
func SplitKV(s, sep string) (string, string) { return TrimCut(s, sep) }
// SplitValid string to slice. will trim each item and filter empty string node.
func SplitValid(s, sep string) (ss []string) { return Split(s, sep) }
// Split string to slice. will trim each item and filter empty string node.
func Split(s, sep string) (ss []string) {
if s = strings.TrimSpace(s); s == "" {
return
}
for _, val := range strings.Split(s, sep) {
if val = strings.TrimSpace(val); val != "" {
ss = append(ss, val)
}
}
return
}
// SplitNValid string to slice. will filter empty string node.
func SplitNValid(s, sep string, n int) (ss []string) { return SplitN(s, sep, n) }
// SplitN string to slice. will filter empty string node.
func SplitN(s, sep string, n int) (ss []string) {
if s = strings.TrimSpace(s); s == "" {
return
}
rawList := strings.Split(s, sep)
for i, val := range rawList {
if val = strings.TrimSpace(val); val != "" {
if len(ss) == n-1 {
ss = append(ss, strings.TrimSpace(strings.Join(rawList[i:], sep)))
break
}
ss = append(ss, val)
}
}
return
}
// SplitTrimmed split string to slice.
// will trim space for each node, but not filter empty
func SplitTrimmed(s, sep string) (ss []string) {
if s = strings.TrimSpace(s); s == "" {
return
}
for _, val := range strings.Split(s, sep) {
ss = append(ss, strings.TrimSpace(val))
}
return
}
// SplitNTrimmed split string to slice.
// will trim space for each node, but not filter empty
func SplitNTrimmed(s, sep string, n int) (ss []string) {
if s = strings.TrimSpace(s); s == "" {
return
}
for _, val := range strings.SplitN(s, sep, n) {
ss = append(ss, strings.TrimSpace(val))
}
return
}
// 根据空白字符(空格,TAB,换行等)分隔字符串
var whitespaceRegexp = regexp.MustCompile("\\s+")
// SplitByWhitespace Separate strings by whitespace characters (space, TAB, newline, etc.)
func SplitByWhitespace(s string) []string {
return whitespaceRegexp.Split(s, -1)
}
// Substr for a string. NOTE: strLn := len(runes)
// if length <= 0, return pos to end.
func Substr(s string, pos, length int) string {
runes := []rune(s)
strLn := len(runes)
// pos is too large
if pos >= strLn {
return ""
}
stopIdx := pos + length
if length == 0 || stopIdx > strLn {
stopIdx = strLn
} else if length < 0 {
stopIdx = strLn + length
}
return string(runes[pos:stopIdx])
}
// SplitInlineComment for an inline text string. default is strict mode.
func SplitInlineComment(val string, strict ...bool) (string, string) {
// strict check: must with a space
if len(strict) == 0 || strict[0] {
if pos := strings.Index(val, " #"); pos > -1 {
return strings.TrimRight(val[0:pos], " "), val[pos+1:]
}
if pos := strings.Index(val, " //"); pos > -1 {
return strings.TrimRight(val[0:pos], " "), val[pos+1:]
}
return val, ""
}
if pos := strings.IndexByte(val, '#'); pos > -1 {
return strings.TrimRight(val[0:pos], " "), val[pos:]
}
if pos := strings.Index(val, "//"); pos > -1 {
return strings.TrimRight(val[0:pos], " "), val[pos:]
}
return val, ""
}
// FirstLine from command output
func FirstLine(output string) string {
if i := strings.IndexByte(output, '\n'); i >= 0 {
return output[0:i]
}
return output
}
+113
View File
@@ -0,0 +1,113 @@
// Package strutil provide some string,char,byte util functions
package strutil
import (
"errors"
"strings"
"github.com/gookit/goutil/comdef"
)
// OrCond return s1 on cond is True, OR return s2.
// Like: cond ? s1 : s2
func OrCond(cond bool, s1, s2 string) string {
if cond {
return s1
}
return s2
}
// BlankOr return default value on val is blank, otherwise return val
func BlankOr(val, defVal string) string {
val = strings.TrimSpace(val)
if val != "" {
return val
}
return defVal
}
// ZeroOr return default value on val is zero, otherwise return val. same of OrElse()
func ZeroOr[T ~string](val, defVal T) T {
if val != "" {
return val
}
return defVal
}
// ErrorOr return default value on err is not nil, otherwise return s
// func ErrorOr(s string, err error, defVal string) string {
// if err != nil {
// return defVal
// }
// return s
// }
// OrElse return default value on s is empty, otherwise return s
func OrElse(s, orVal string) string {
if s != "" {
return s
}
return orVal
}
// OrElseNilSafe return default value on s is nil, otherwise return s
func OrElseNilSafe(s *string, orVal string) string {
if s == nil || *s == "" {
return orVal
}
return *s
}
// OrHandle return fn(s) on s is not empty.
func OrHandle(s string, fn comdef.StringHandleFunc) string {
if s != "" {
return fn(s)
}
return s
}
// Valid return first not empty element.
func Valid(ss ...string) string {
for _, s := range ss {
if s != "" {
return s
}
}
return ""
}
// SubstrCount returns the number of times the substr substring occurs in the s string.
// Actually, it comes from strings.Count().
//
// - s The string to search in
// - substr The substring to search for
// - params[0] The offset where to start counting.
// - params[1] The maximum length after the specified offset to search for the substring.
func SubstrCount(s, substr string, params ...uint64) (int, error) {
larg := len(params)
hasArgs := larg != 0
if hasArgs && larg > 2 {
return 0, errors.New("too many parameters")
}
if !hasArgs {
return strings.Count(s, substr), nil
}
strlen := len(s)
offset := 0
end := strlen
if hasArgs {
offset = int(params[0])
if larg == 2 {
length := int(params[1])
end = offset + length
}
if end > strlen {
end = strlen
}
}
s = string([]rune(s)[offset:end])
return strings.Count(s, substr), nil
}
+277
View File
@@ -0,0 +1,277 @@
package textutil
import (
"fmt"
"io"
"regexp"
"strings"
"text/template"
"github.com/gookit/goutil/arrutil"
"github.com/gookit/goutil/fsutil"
"github.com/gookit/goutil/reflects"
"github.com/gookit/goutil/structs"
"github.com/gookit/goutil/strutil"
"github.com/gookit/goutil/x/basefn"
)
// LTemplateOptFn lite template option func
type LTemplateOptFn func(opt *LiteTemplateOpt)
// LiteTemplateOpt template options for LiteTemplate
type LiteTemplateOpt struct {
// func name alias map. eg: {"up_first": "upFirst"}
nameMp structs.Aliases
Funcs template.FuncMap
Left, Right string
ParseDef bool
ParseEnv bool
}
// SetVarFmt custom sets the variable format in template
func (o *LiteTemplateOpt) SetVarFmt(varFmt string) {
o.Left, o.Right = strutil.TrimCut(varFmt, ",")
}
// LiteTemplate implement a simple text template engine.
//
// - support parse template vars
// - support access multi-level map field. eg: {{ user.name }}
// - support parse default value
// - support parse env vars
// - support custom pipeline func handle. eg: {{ name | upper }} {{ name | def:guest }}
//
// NOTE: not support control flow, eg: if/else/for/with
type LiteTemplate struct {
LiteTemplateOpt
vr VarReplacer
// template func map. refer the text/template
//
// Func allow return 1 or 2 values, if return 2 values, the second value is error.
fxs map[string]*reflects.FuncX
}
// NewLiteTemplate instance
func NewLiteTemplate(opFns ...LTemplateOptFn) *LiteTemplate {
st := &LiteTemplate{
fxs: make(map[string]*reflects.FuncX),
// with default options
LiteTemplateOpt: LiteTemplateOpt{
Left: "{{",
Right: "}}",
ParseDef: true,
ParseEnv: true,
},
}
st.vr.RenderFn = st.renderVars
for _, fn := range opFns {
fn(&st.LiteTemplateOpt)
}
st.Init()
return st
}
// Init LiteTemplate
func (t *LiteTemplate) Init() {
if t.vr.init {
return
}
// init var replacer
t.vr.init = true
t.initReplacer(&t.vr)
// add built-in funcs
t.AddFuncs(builtInFuncs)
t.nameMp.AddAliasMap(map[string]string{
"up_first": "upFirst",
"lc_first": "lcFirst",
"def": "default",
})
// add custom funcs
if len(t.Funcs) > 0 {
t.AddFuncs(t.Funcs)
}
}
func (t *LiteTemplate) initReplacer(vr *VarReplacer) {
vr.flatSubs = true
vr.parseDef = t.ParseDef
vr.parseEnv = t.ParseEnv
vr.Left, vr.Right = t.Left, t.Right
basefn.PanicIf(vr.Right == "", "var format right chars is required")
vr.lLen, vr.rLen = len(vr.Left), len(vr.Right)
rightLast := string(vr.Right[vr.rLen-1]) // 排除匹配,防止匹配到类似 "{} adb ddf {var}"
// eg: \{(?s:([^\}]+?))\}
// (?s:...) - 让 "." 匹配换行
// (?s:(.+?)) - 第二个 "?" 非贪婪匹配
pattern := regexp.QuoteMeta(vr.Left) + `(?s:([^` + regexp.QuoteMeta(rightLast) + `]+?))` + regexp.QuoteMeta(vr.Right)
vr.varReg = regexp.MustCompile(pattern)
}
// AddFuncs add custom template functions
func (t *LiteTemplate) AddFuncs(fns map[string]any) {
for name, fn := range fns {
t.fxs[name] = reflects.NewFunc(fn)
}
}
// RenderString render template string with vars
func (t *LiteTemplate) RenderString(s string, vars map[string]any) string {
return t.vr.Replace(s, vars)
}
// RenderFile render template file with vars
func (t *LiteTemplate) RenderFile(filePath string, vars map[string]any) (string, error) {
// read file contents
s, err := fsutil.ReadStringOrErr(filePath)
if err != nil {
return "", err
}
return t.vr.Replace(s, vars), nil
}
// RenderWrite render template string with vars, and write result to writer
func (t *LiteTemplate) RenderWrite(wr io.Writer, s string, vars map[string]any) error {
s = t.vr.Replace(s, vars)
_, err := io.WriteString(wr, s)
return err
}
func (t *LiteTemplate) renderVars(s string, varMap map[string]string) string {
return t.vr.varReg.ReplaceAllStringFunc(s, func(sub string) string {
// var name or pipe expression.
name := strings.TrimSpace(sub[t.vr.lLen : len(sub)-t.vr.rLen])
name = strings.TrimLeft(name, "$.")
var defVal string
var pipes []string
if strings.ContainsRune(name, '|') {
pipes = strutil.Split(name, "|")
// compatible default value. eg: {{ name | inhere }}
if len(pipes) == 2 && !strings.ContainsRune(pipes[1], ':') && !t.isFunc(pipes[1]) {
name, defVal = pipes[0], pipes[1]
pipes = nil // clear pipes
} else { // collect pipe functions
name, pipes = pipes[0], pipes[1:]
}
}
if val, ok := varMap[name]; ok {
if len(pipes) > 0 {
var err error
val, err = t.applyPipes(val, pipes)
if err != nil {
return fmt.Sprintf("Render var %q error: %v", name, err)
}
}
return val
}
// var not found
if len(defVal) > 0 {
return defVal
}
if t.vr.NotFound != nil {
if val, ok := t.vr.NotFound(name); ok {
return val
}
}
// check is default func. eg: {{ name | def:guest }}
if len(pipes) == 1 && strings.ContainsRune(pipes[0], ':') {
fName, argVal := strutil.TrimCut(pipes[0], ":")
if t.isDefaultFunc(fName) {
return argVal
}
}
t.vr.missVars = append(t.vr.missVars, name)
return sub
})
}
func (t *LiteTemplate) applyPipes(val any, pipes []string) (string, error) {
var err error
// pipe expr: "trim|upper|substr:1,2"
// =>
// pipes: ["trim", "upper", "substr:1,2"]
for _, name := range pipes {
args := []any{val}
// has custom args. eg: "substr:1,2"
if strings.ContainsRune(name, ':') {
var argStr string
name, argStr = strutil.TrimCut(name, ":")
if otherArgs := parseArgStr(argStr); len(otherArgs) > 0 {
args = append(args, otherArgs...)
}
}
name = t.nameMp.ResolveAlias(name)
// call pipe func
if fx, ok := t.fxs[name]; ok {
val, err = fx.Call2(args...)
if err != nil {
return "", err
}
} else {
return "", fmt.Errorf("template func %q not found", name)
}
}
return strutil.ToString(val)
}
func (t *LiteTemplate) isFunc(name string) bool {
_, ok := t.fxs[name]
if !ok {
// check name alias
return t.nameMp.HasAlias(name)
}
return ok
}
func (t *LiteTemplate) isDefaultFunc(name string) bool {
return name == "default" || name == "def"
}
var stdTpl = NewLiteTemplate()
// RenderFile render template file with vars
func RenderFile(filePath string, vars map[string]any) (string, error) {
return stdTpl.RenderFile(filePath, vars)
}
// RenderString render str template string or file.
func RenderString(input string, data map[string]any) string {
return stdTpl.RenderString(input, data)
}
// RenderWrite render template string with vars, and write result to writer
func RenderWrite(wr io.Writer, s string, vars map[string]any) error {
return stdTpl.RenderWrite(wr, s, vars)
}
func parseArgStr(argStr string) (ss []any) {
if argStr == "" { // no arg
return
}
if len(argStr) == 1 { // one char
return []any{argStr}
}
return arrutil.StringsToAnys(strutil.Split(argStr, ","))
}
+181
View File
@@ -0,0 +1,181 @@
package textutil
import (
"fmt"
"regexp"
"strings"
"github.com/gookit/goutil/maputil"
"github.com/gookit/goutil/reflects"
"github.com/gookit/goutil/strutil"
)
// type SimpleAnyFunc func(args ...any) any
// StrVarRenderer implements like shell vars renderer
// 简单的实现类似 php, kotlin, shell 插值变量渲染,表达式解析处理。
//
// - var format: $var_name, ${some_var}, ${top.sub_var}
// - func call: ${func($var_name, 'const string')}
type StrVarRenderer struct {
// global variables
vars map[string]any
// fallback func for var not exists
getter FallbackFn
// funcMap map[string]any TODO use any, add reflect value to rfs
funcMap map[string]func(...any) any
// var func map. refer the text/template TODO
//
// Func allow return 1 or 2 values, if return 2 values, the second value is error.
rfs map[string]*reflects.FuncX
}
// NewStrVarRenderer create a new StrVarRenderer
func NewStrVarRenderer() *StrVarRenderer {
return &StrVarRenderer{
vars: make(map[string]any),
// funcMap: make(map[string]any),
funcMap: make(map[string]func(...any) any),
}
}
// SetVars set variables
func (r *StrVarRenderer) SetVars(vars map[string]any) *StrVarRenderer {
for k, v := range vars {
r.vars[k] = v
}
return r
}
// SetVar set a variable
func (r *StrVarRenderer) SetVar(name string, value any) *StrVarRenderer {
r.vars[name] = value
return r
}
// SetFuncMap set function map
func (r *StrVarRenderer) SetFuncMap(funcMap map[string]func(...any) any) *StrVarRenderer {
for k, v := range funcMap {
r.funcMap[k] = v
}
return r
}
// SetFunc set a function
func (r *StrVarRenderer) SetFunc(name string, fn func(...any) any) *StrVarRenderer {
r.funcMap[name] = fn
return r
}
// SetGetter set variable getter
func (r *StrVarRenderer) SetGetter(getter FallbackFn) *StrVarRenderer {
r.getter = getter
return r
}
var (
// 处理 $var_name 格式
// - 允许:$1..$N 这样的变量
// - 也支持 $@, $* 变量
reS = regexp.MustCompile(`\$(\w[a-zA-Z0-9_]*|[@|*])`)
// 处理 ${var_name} ${top.sub} 格式
reQ = regexp.MustCompile(`\$\{([a-zA-Z][a-zA-Z0-9_.]*)\}`)
// 处理 ${func(...)} 格式
reFn = regexp.MustCompile(`\$\{([a-zA-Z][a-zA-Z0-9_]*)\(([^}]*)\)\}`)
)
// Render rendering input string with variables
func (r *StrVarRenderer) Render(input string, vars map[string]any) string {
vars = maputil.Merge1level(r.vars, vars)
data := maputil.Map(vars)
// 处理 $var_name 格式
input = r.replaceVars(input, reS, data)
if strings.Contains(input, "${") {
// 处理 ${var.name} 格式
input = r.replaceVars(input, reQ, data)
// 处理 ${func(...)} 格式
input = r.handleFuncCalls(input, reFn, data)
}
return input
}
func (r *StrVarRenderer) handleFuncCalls(input string, re *regexp.Regexp, vars maputil.Map) string {
return re.ReplaceAllStringFunc(input, func(matched string) string {
submatch := re.FindStringSubmatch(matched)
funcName := submatch[1]
// 解析参数 并 调用函数
if fn, ok := r.funcMap[funcName]; ok {
args := r.parseArgs(submatch[2], vars)
result := fn(args...)
return fmt.Sprint(result)
}
return matched
})
}
func (r *StrVarRenderer) parseArgs(argsStr string, data maputil.Map) []any {
if argsStr == "" {
return []any{}
}
// 简单参数解析,按逗号分割
args := strings.Split(argsStr, ",")
results := make([]any, len(args))
for i, arg := range args {
arg = strings.TrimSpace(arg)
// is var name
if arg[0] == '$' {
results[i] = data.Get(arg[1:])
continue
}
// 常量值:去掉引号
last := len(arg) - 1
// strconv.Unquote( arg)
if (arg[0] == '"' && arg[last] == '"') || (arg[0] == '\'' && arg[last] == '\'') {
results[i] = arg[1 : len(arg)-1]
} else if arg == "true" || arg == "false" {
results[i] = strutil.SafeBool(arg)
} else if strutil.IsInt(arg) {
results[i] = strutil.SafeInt(arg)
} else {
results[i] = arg
}
}
return results
}
func (r *StrVarRenderer) replaceVars(input string, re *regexp.Regexp, data maputil.Map) string {
return re.ReplaceAllStringFunc(input, func(matched string) string {
var varName string
// format: ${var_name} 提取变量名
if strings.HasPrefix(matched, "${") {
varName = matched[2 : len(matched)-1]
} else {
// format: $var_name
varName = matched[1:]
}
// 从 vars map 获取值,支持嵌套变量名
if val, ok := data.GetByPath(varName); ok {
return fmt.Sprint(val)
}
// fallback: 使用 getter fn 获取值
if r.getter != nil {
if val, ok := r.getter(varName); ok {
return fmt.Sprint(val)
}
}
return matched
})
}
+90
View File
@@ -0,0 +1,90 @@
package textutil
import (
"bytes"
"io"
"strings"
"text/template"
"github.com/gookit/goutil"
"github.com/gookit/goutil/fsutil"
"github.com/gookit/goutil/strutil"
"github.com/gookit/goutil/x/basefn"
)
var builtInFuncs = template.FuncMap{
// don't escape content
"raw": func(s string) string {
return s
},
"trim": strings.TrimSpace,
// lower first char
"lcFirst": strutil.LowerFirst,
// upper first char
"upFirst": strutil.UpperFirst,
// upper case
"upper": strings.ToUpper,
// lower case
"lower": strings.ToLower,
// cut sub-string
"substr": strutil.Substr,
// default value
"default": func(v, defVal any) string {
if goutil.IsEmpty(v) {
return strutil.SafeString(defVal)
}
return strutil.SafeString(v)
},
// join strings
"join": func(ss []string, sep string) string {
return strings.Join(ss, sep)
},
}
// TextRenderOpt render text template options
type TextRenderOpt struct {
// Output use custom output writer
Output io.Writer
// Funcs add custom template functions
Funcs template.FuncMap
}
// RenderOptFn render option func
type RenderOptFn func(opt *TextRenderOpt)
// NewRenderOpt create a new render options
func NewRenderOpt(optFns []RenderOptFn) *TextRenderOpt {
opt := &TextRenderOpt{}
for _, fn := range optFns {
fn(opt)
}
return opt
}
// RenderGoTpl render input text or template file.
func RenderGoTpl(input string, data any, optFns ...RenderOptFn) string {
opt := NewRenderOpt(optFns)
t := template.New("text-renderer")
t.Funcs(builtInFuncs)
if len(opt.Funcs) > 0 {
t.Funcs(opt.Funcs)
}
if !strings.Contains(input, "{{") && fsutil.IsFile(input) {
template.Must(t.ParseFiles(input))
} else {
template.Must(t.Parse(input))
}
// use custom output writer
if opt.Output != nil {
basefn.MustOK(t.Execute(opt.Output, data))
return "" // return empty string
}
// use buffer receive rendered content
buf := new(bytes.Buffer)
basefn.MustOK(t.Execute(buf, data))
return buf.String()
}
+78
View File
@@ -0,0 +1,78 @@
// Package textutil provide some extensions text handle util functions.
package textutil
import (
"fmt"
"strings"
"github.com/gookit/goutil/arrutil"
"github.com/gookit/goutil/internal/comfunc"
"github.com/gookit/goutil/maputil"
"github.com/gookit/goutil/strutil"
)
// ReplaceVars by regex replace given tpl vars.
//
// If a format is empty, will use {const DefaultVarFormat}
func ReplaceVars(text string, vars map[string]any, format string) string {
return NewVarReplacer(format).Replace(text, vars)
}
// RenderSMap by regex replacement given tpl vars.
//
// If a format is empty, will use {const DefaultVarFormat}
func RenderSMap(text string, vars map[string]string, format string) string {
return NewVarReplacer(format).RenderSimple(text, vars)
}
// IsMatchAll keywords in the give text string.
//
// TIP: can use ^ for exclude match.
func IsMatchAll(s string, keywords []string) bool {
return strutil.SimpleMatch(s, keywords)
}
// ParseInlineINI parse config string to string-map. it's like INI format contents.
//
// Examples:
//
// eg: "name=val0;shorts=i;required=true;desc=a message"
// =>
// {name: val0, shorts: i, required: true, desc: a message}
func ParseInlineINI(tagVal string, keys ...string) (mp maputil.SMap, err error) {
ss := strutil.Split(tagVal, ";")
ln := len(ss)
if ln == 0 {
return
}
mp = make(maputil.SMap, ln)
for _, s := range ss {
if !strings.ContainsRune(s, '=') {
err = fmt.Errorf("parse inline config error: must match `KEY=VAL`")
return
}
key, val := strutil.TrimCut(s, "=")
if len(keys) > 0 && !arrutil.StringsHas(keys, key) {
err = fmt.Errorf("parse inline config error: invalid key name %q", key)
return
}
mp[key] = val
}
return
}
// ParseSimpleINI parse simple multiline config string to a string-map.
// Can use to parse simple INI or dotenv file contents.
//
// NOTE:
//
// - it's like INI format contents.
// - support comments line with: "#", ";", "//"
// - support inline comments with: " #" eg: name=tom # a comments
// - DON'T support submap parse.
func ParseSimpleINI(text string) (mp maputil.SMap, err error) {
return comfunc.ParseEnvLines(text, comfunc.ParseEnvLineOption{})
}
+251
View File
@@ -0,0 +1,251 @@
package textutil
import (
"os"
"reflect"
"regexp"
"strings"
"github.com/gookit/goutil/arrutil"
"github.com/gookit/goutil/internal/varexpr"
"github.com/gookit/goutil/maputil"
"github.com/gookit/goutil/strutil"
)
// DefaultVarFormat var template
const DefaultVarFormat = "{{,}}"
// FallbackFn type
type FallbackFn = func(name string) (val string, ok bool)
// VarReplacer struct
type VarReplacer struct {
init bool
Left, Right string
lLen, rLen int
varReg *regexp.Regexp
// flatten sub map in vars. default: true
//
// eg: {name: {a: 1, b: 2}} => {name.a: 1, name.b: 2}
flatSubs bool
// do parse env value in var-value and tpl var-name. default: false
parseEnv bool
// do parse default value. default: false
//
// eg: {{ name | inhere }}
parseDef bool
// keepMissVars list.
//
// default: False - will clear on each replacement
keepMissVars bool
// missing vars list
missVars []string
// NotFound hook func. on var-name not found
NotFound FallbackFn
// RenderFn custom render func
RenderFn func(s string, vs map[string]string) string
}
// NewVarReplacer instance. default format is: DefaultVarFormat
//
// Usage:
//
// rpl := NewVarReplacer("{{,}}") // access var: {{ var }}, {{ top.sub }}
// // or
// rpl := NewVarReplacer("$") // access var: $var, $top.sub
func NewVarReplacer(format string, opFns ...func(vp *VarReplacer)) *VarReplacer {
vp := &VarReplacer{flatSubs: true}
for _, fn := range opFns {
fn(vp)
}
return vp.WithFormat(format)
}
// NewFullReplacer instance. will enable parse env and parse default.
//
// Usage:
//
// rpl := NewFullReplacer("{{,}}")
func NewFullReplacer(format string) *VarReplacer {
return NewVarReplacer(format, func(vp *VarReplacer) {
vp.WithParseEnv().WithParseDefault().KeepMissingVars()
})
}
// DisableFlatten on the input vars map
func (r *VarReplacer) DisableFlatten() *VarReplacer {
r.flatSubs = false
return r
}
// KeepMissingVars on the replacement handle
func (r *VarReplacer) KeepMissingVars() *VarReplacer {
r.keepMissVars = true
return r
}
// WithParseDefault value on the input template contents. eg: {{ name | inhere }}
func (r *VarReplacer) WithParseDefault() *VarReplacer {
r.parseDef = true
return r
}
// WithParseEnv on the input vars value
func (r *VarReplacer) WithParseEnv() *VarReplacer {
r.parseEnv = true
return r
}
// OnNotFound var handle func
func (r *VarReplacer) OnNotFound(fn FallbackFn) *VarReplacer {
r.NotFound = fn
return r
}
// WithFormat custom var template
func (r *VarReplacer) WithFormat(format string) *VarReplacer {
r.Left, r.Right = strutil.QuietCut(strutil.OrElse(format, DefaultVarFormat), ",")
r.Init()
return r
}
// Init var replacer
func (r *VarReplacer) Init() {
if !r.init {
r.lLen, r.rLen = len(r.Left), len(r.Right)
if r.Right != "" {
r.varReg = regexp.MustCompile(regexp.QuoteMeta(r.Left) + `([\w\s\|.-]+)` + regexp.QuoteMeta(r.Right))
} else {
// no right tag. eg: $name, $user.age
r.varReg = regexp.MustCompile(regexp.QuoteMeta(r.Left) + `(\w[\w-]*(?:\.[\w-]+)*)`)
}
r.init = true
}
}
// ParseVars parse the text contents and collect vars
func (r *VarReplacer) ParseVars(s string) []string {
ss := arrutil.StringsMap(r.varReg.FindAllString(s, -1), func(val string) string {
return strings.TrimSpace(val[r.lLen : len(val)-r.rLen])
})
return arrutil.Unique(ss)
}
// Replace any-map vars in the text contents
func (r *VarReplacer) Replace(s string, tplVars map[string]any) string {
return r.Render(s, tplVars)
}
// Render any-map vars in the text contents
func (r *VarReplacer) Render(s string, tplVars map[string]any) string {
if !strings.Contains(s, r.Left) {
return s
}
if !r.parseDef && len(tplVars) == 0 {
return s
}
r.Init()
var varMap map[string]string
if r.flatSubs {
varMap = make(map[string]string, len(tplVars)*2)
maputil.FlatWithFunc(tplVars, func(path string, val reflect.Value) {
if val.Kind() == reflect.String {
if r.parseEnv {
varMap[path] = varexpr.SafeParse(val.String())
} else {
varMap[path] = val.String()
}
} else {
varMap[path] = strutil.QuietString(val.Interface())
}
})
} else {
varMap = maputil.ToStringMap(tplVars)
}
return r.doReplace(s, varMap)
}
// ReplaceSMap string-map vars in the text contents
func (r *VarReplacer) ReplaceSMap(s string, varMap map[string]string) string {
return r.RenderSimple(s, varMap)
}
// RenderSimple string-map vars in the text contents. alias of ReplaceSMap()
func (r *VarReplacer) RenderSimple(s string, varMap map[string]string) string {
if len(varMap) == 0 || !strings.Contains(s, r.Left) {
return s
}
if r.parseEnv {
for name, val := range varMap {
if strings.Contains(val, "${") {
varMap[name] = varexpr.SafeParse(val)
} else {
varMap[name] = val
}
}
}
r.Init()
return r.doReplace(s, varMap)
}
// MissVars list
func (r *VarReplacer) MissVars() []string {
return r.missVars
}
// ResetMissVars list
func (r *VarReplacer) ResetMissVars() {
r.missVars = make([]string, 0)
}
// Replace string-map vars in the text contents
func (r *VarReplacer) doReplace(s string, varMap map[string]string) string {
if !r.keepMissVars {
r.missVars = make([]string, 0) // clear on each replacement
}
// use custom render func
if r.RenderFn != nil {
return r.RenderFn(s, varMap)
}
return r.varReg.ReplaceAllStringFunc(s, func(sub string) string {
name := strings.TrimSpace(sub[r.lLen : len(sub)-r.rLen])
var defVal string
if r.parseDef && strings.ContainsRune(name, '|') {
name, defVal = strutil.TrimCut(name, "|")
}
if val, ok := varMap[name]; ok {
return val
}
if r.parseEnv && strutil.IsEnvName(name) {
if val := os.Getenv(name); val != "" {
return val
}
}
// has custom not found handle func
if r.NotFound != nil {
if val, ok := r.NotFound(name); ok {
return val
}
}
if len(defVal) > 0 {
return defVal
}
r.missVars = append(r.missVars, name)
return sub
})
}
+98
View File
@@ -0,0 +1,98 @@
package strutil
import "strings"
// Value string
type Value string
// StrVal string. alias of Value
type StrVal = Value
// Set value
func (s *Value) Set(val string) error {
*s = Value(val)
return nil
}
// IsEmpty check
func (s Value) IsEmpty() bool {
return string(s) == ""
}
// IsBlank check
func (s Value) IsBlank() bool {
return strings.TrimSpace(string(s)) == ""
}
// IsStartWith prefix
func (s Value) IsStartWith(sub string) bool {
return strings.HasPrefix(string(s), sub)
}
// HasPrefix prefix
func (s Value) HasPrefix(sub string) bool {
return strings.HasPrefix(string(s), sub)
}
// IsEndWith suffix
func (s Value) IsEndWith(sub string) bool {
return strings.HasSuffix(string(s), sub)
}
// HasSuffix suffix
func (s Value) HasSuffix(sub string) bool {
return strings.HasSuffix(string(s), sub)
}
// Bytes string to bytes
func (s Value) Bytes() []byte {
return []byte(s)
}
// Val string
func (s Value) Val() string {
return string(s)
}
// Int convert
func (s Value) Int() int {
return QuietInt(string(s))
}
// Int64 convert
func (s Value) Int64() int64 {
return QuietInt64(string(s))
}
// Bool convert
func (s Value) Bool() bool {
return QuietBool(string(s))
}
// Value string
func (s Value) String() string {
return string(s)
}
// OrElse string
func (s Value) OrElse(or string) string {
if s != "" {
return string(s)
}
return or
}
// Split string
func (s Value) Split(sep string) []string {
return strings.Split(string(s), sep)
}
// SplitN string
func (s Value) SplitN(sep string, n int) []string {
return strings.SplitN(string(s), sep, n)
}
// WithTrimSpace string and return new
func (s Value) WithTrimSpace() Value {
return Value(strings.TrimSpace(string(s)))
}