diff --git a/go.mod b/go.mod index b1e234230..cf6a3f1cf 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( github.com/urfave/cli/v2 v2.25.7 go.uber.org/atomic v1.11.0 golang.org/x/crypto v0.13.0 + golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 google.golang.org/grpc v1.58.2 gorm.io/driver/mysql v1.5.1 gorm.io/driver/postgres v1.5.2 diff --git a/go.sum b/go.sum index 5ce00a18a..089973e21 100644 --- a/go.sum +++ b/go.sum @@ -623,6 +623,7 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/support/str/str.go b/support/str/str.go index 39b4d3ae1..37f3b6243 100644 --- a/support/str/str.go +++ b/support/str/str.go @@ -3,11 +3,927 @@ package str import ( "bytes" "crypto/rand" + "encoding/json" + "path/filepath" + "regexp" "strconv" "strings" "unicode" + "unicode/utf8" + + "golang.org/x/exp/constraints" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) +type String struct { + value string +} + +// ExcerptOption is the option for Excerpt method +type ExcerptOption struct { + Radius int + Omission string +} + +// Of creates a new String instance with the given value. +func Of(value string) *String { + return &String{value: value} +} + +// After returns a new String instance with the substring after the first occurrence of the specified search string. +func (s *String) After(search string) *String { + if search == "" { + return s + } + index := strings.Index(s.value, search) + if index != -1 { + s.value = s.value[index+len(search):] + } + return s +} + +// AfterLast returns the String instance with the substring after the last occurrence of the specified search string. +func (s *String) AfterLast(search string) *String { + index := strings.LastIndex(s.value, search) + if index != -1 { + s.value = s.value[index+len(search):] + } + + return s +} + +// Append appends one or more strings to the current string. +func (s *String) Append(values ...string) *String { + s.value += strings.Join(values, "") + return s +} + +// Basename returns the String instance with the basename of the current file path string, +// and trims the suffix based on the parameter(optional). +func (s *String) Basename(suffix ...string) *String { + s.value = filepath.Base(s.value) + if len(suffix) > 0 && suffix[0] != "" { + s.value = strings.TrimSuffix(s.value, suffix[0]) + } + return s +} + +// Before returns the String instance with the substring before the first occurrence of the specified search string. +func (s *String) Before(search string) *String { + index := strings.Index(s.value, search) + if index != -1 { + s.value = s.value[:index] + } + + return s +} + +// BeforeLast returns the String instance with the substring before the last occurrence of the specified search string. +func (s *String) BeforeLast(search string) *String { + index := strings.LastIndex(s.value, search) + if index != -1 { + s.value = s.value[:index] + } + + return s +} + +// Between returns the String instance with the substring between the given start and end strings. +func (s *String) Between(start, end string) *String { + if start == "" || end == "" { + return s + } + return s.After(start).BeforeLast(end) +} + +// BetweenFirst returns the String instance with the substring between the first occurrence of the given start string and the given end string. +func (s *String) BetweenFirst(start, end string) *String { + if start == "" || end == "" { + return s + } + return s.Before(end).After(start) +} + +// Camel returns the String instance in camel case. +func (s *String) Camel() *String { + return s.Studly().LcFirst() +} + +// CharAt returns the character at the specified index. +func (s *String) CharAt(index int) string { + length := len(s.value) + // return zero string when char doesn't exists + if index < 0 && index < -length || index > length-1 { + return "" + } + return Substr(s.value, index, 1) +} + +// Contains returns true if the string contains the given value or any of the values. +func (s *String) Contains(values ...string) bool { + for _, value := range values { + if value != "" && strings.Contains(s.value, value) { + return true + } + } + + return false +} + +// ContainsAll returns true if the string contains all of the given values. +func (s *String) ContainsAll(values ...string) bool { + for _, value := range values { + if !strings.Contains(s.value, value) { + return false + } + } + + return true +} + +// Dirname returns the String instance with the directory name of the current file path string. +func (s *String) Dirname(levels ...int) *String { + defaultLevels := 1 + if len(levels) > 0 { + defaultLevels = levels[0] + } + + dir := s.value + for i := 0; i < defaultLevels; i++ { + dir = filepath.Dir(dir) + } + + s.value = dir + return s +} + +// EndsWith returns true if the string ends with the given value or any of the values. +func (s *String) EndsWith(values ...string) bool { + for _, value := range values { + if value != "" && strings.HasSuffix(s.value, value) { + return true + } + } + + return false +} + +// Exactly returns true if the string is exactly the given value. +func (s *String) Exactly(value string) bool { + return s.value == value +} + +// Excerpt returns the String instance truncated to the given length. +func (s *String) Excerpt(phrase string, options ...ExcerptOption) *String { + defaultOptions := ExcerptOption{ + Radius: 100, + Omission: "...", + } + + if len(options) > 0 { + if options[0].Radius != 0 { + defaultOptions.Radius = options[0].Radius + } + if options[0].Omission != "" { + defaultOptions.Omission = options[0].Omission + } + } + + radius := maximum(0, defaultOptions.Radius) + omission := defaultOptions.Omission + + regex := regexp.MustCompile(`(.*?)(` + regexp.QuoteMeta(phrase) + `)(.*)`) + matches := regex.FindStringSubmatch(s.value) + + if len(matches) == 0 { + return s + } + + start := strings.TrimRight(matches[1], "") + end := strings.TrimLeft(matches[3], "") + + end = Of(Substr(end, 0, radius)).LTrim(""). + Unless(func(s *String) bool { + return s.Exactly(end) + }, func(s *String) *String { + return s.Append(omission) + }).String() + + s.value = Of(Substr(start, maximum(len(start)-radius, 0), radius)).LTrim(""). + Unless(func(s *String) bool { + return s.Exactly(start) + }, func(s *String) *String { + return s.Prepend(omission) + }).Append(matches[2], end).String() + + return s +} + +// Explode splits the string by given delimiter string. +func (s *String) Explode(delimiter string, limit ...int) []string { + defaultLimit := 1 + isLimitSet := false + if len(limit) > 0 && limit[0] != 0 { + defaultLimit = limit[0] + isLimitSet = true + } + tempExplode := strings.Split(s.value, delimiter) + if !isLimitSet || len(tempExplode) <= defaultLimit { + return tempExplode + } + + if defaultLimit > 0 { + return append(tempExplode[:defaultLimit-1], strings.Join(tempExplode[defaultLimit-1:], delimiter)) + } + + if defaultLimit < 0 && len(tempExplode) <= -defaultLimit { + return []string{} + } + + return tempExplode[:len(tempExplode)+defaultLimit] +} + +// Finish returns the String instance with the given value appended. +// If the given value already ends with the suffix, it will not be added twice. +func (s *String) Finish(value string) *String { + quoted := regexp.QuoteMeta(value) + reg := regexp.MustCompile("(?:" + quoted + ")+$") + s.value = reg.ReplaceAllString(s.value, "") + value + return s +} + +// Headline returns the String instance in headline case. +func (s *String) Headline() *String { + parts := s.Explode(" ") + + if len(parts) > 1 { + return s.Title() + } + + parts = Of(strings.Join(parts, "_")).Studly().UcSplit() + collapsed := Of(strings.Join(parts, "_")). + Replace("-", "_"). + Replace(" ", "_"). + Replace("_", "_").Explode("_") + + s.value = strings.Join(collapsed, " ") + return s +} + +// Is returns true if the string matches any of the given patterns. +func (s *String) Is(patterns ...string) bool { + for _, pattern := range patterns { + if pattern == s.value { + return true + } + + // Escape special characters in the pattern + pattern = regexp.QuoteMeta(pattern) + + // Replace asterisks with regular expression wildcards + pattern = strings.ReplaceAll(pattern, `\*`, ".*") + + // Create a regular expression pattern for matching + regexPattern := "^" + pattern + "$" + + // Compile the regular expression + regex := regexp.MustCompile(regexPattern) + + // Check if the value matches the pattern + if regex.MatchString(s.value) { + return true + } + } + + return false +} + +// IsEmpty returns true if the string is empty. +func (s *String) IsEmpty() bool { + return s.value == "" +} + +// IsNotEmpty returns true if the string is not empty. +func (s *String) IsNotEmpty() bool { + return !s.IsEmpty() +} + +// IsAscii returns true if the string contains only ASCII characters. +func (s *String) IsAscii() bool { + return s.IsMatch(`^[\x00-\x7F]+$`) +} + +// IsMap returns true if the string is a valid Map. +func (s *String) IsMap() bool { + var obj map[string]interface{} + return json.Unmarshal([]byte(s.value), &obj) == nil +} + +// IsSlice returns true if the string is a valid Slice. +func (s *String) IsSlice() bool { + var arr []interface{} + return json.Unmarshal([]byte(s.value), &arr) == nil +} + +// IsUlid returns true if the string is a valid ULID. +func (s *String) IsUlid() bool { + return s.IsMatch(`^[0-9A-Z]{26}$`) +} + +// IsUuid returns true if the string is a valid UUID. +func (s *String) IsUuid() bool { + return s.IsMatch(`(?i)^[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}$`) +} + +// Kebab returns the String instance in kebab case. +func (s *String) Kebab() *String { + return s.Snake("-") +} + +// LcFirst returns the String instance with the first character lowercased. +func (s *String) LcFirst() *String { + if s.Length() == 0 { + return s + } + s.value = strings.ToLower(Substr(s.value, 0, 1)) + Substr(s.value, 1) + return s +} + +// Length returns the length of the string. +func (s *String) Length() int { + return utf8.RuneCountInString(s.value) +} + +// Limit returns the String instance truncated to the given length. +func (s *String) Limit(limit int, end ...string) *String { + defaultEnd := "..." + if len(end) > 0 { + defaultEnd = end[0] + } + + if s.Length() <= limit { + return s + } + s.value = Substr(s.value, 0, limit) + defaultEnd + return s +} + +// Lower returns the String instance in lower case. +func (s *String) Lower() *String { + s.value = strings.ToLower(s.value) + return s +} + +// Ltrim returns the String instance with the leftmost occurrence of the given value removed. +func (s *String) LTrim(characters ...string) *String { + if len(characters) == 0 { + s.value = strings.TrimLeft(s.value, " ") + return s + } + + s.value = strings.TrimLeft(s.value, characters[0]) + return s +} + +// Mask returns the String instance with the given character masking the specified number of characters. +func (s *String) Mask(character string, index int, length ...int) *String { + // Check if the character is empty, if so, return the original string. + if character == "" { + return s + } + + segment := Substr(s.value, index, length...) + + // Check if the segment is empty, if so, return the original string. + if segment == "" { + return s + } + + strLen := utf8.RuneCountInString(s.value) + startIndex := index + + // Check if the start index is out of bounds. + if index < 0 { + if index < -strLen { + startIndex = 0 + } else { + startIndex = strLen + index + } + } + + start := Substr(s.value, 0, startIndex) + segmentLen := utf8.RuneCountInString(segment) + end := Substr(s.value, startIndex+segmentLen) + + s.value = start + strings.Repeat(Substr(character, 0, 1), segmentLen) + end + return s +} + +// Match returns the String instance with the first occurrence of the given pattern. +func (s *String) Match(pattern string) *String { + if pattern == "" { + return s + } + reg := regexp.MustCompile(pattern) + s.value = reg.FindString(s.value) + return s +} + +// MatchAll returns all matches for the given regular expression. +func (s *String) MatchAll(pattern string) []string { + if pattern == "" { + return []string{s.value} + } + reg := regexp.MustCompile(pattern) + return reg.FindAllString(s.value, -1) +} + +// IsMatch returns true if the string matches any of the given patterns. +func (s *String) IsMatch(patterns ...string) bool { + for _, pattern := range patterns { + reg := regexp.MustCompile(pattern) + if reg.MatchString(s.value) { + return true + } + } + + return false +} + +// NewLine appends one or more new lines to the current string. +func (s *String) NewLine(count ...int) *String { + if len(count) == 0 { + s.value += "\n" + return s + } + + s.value += strings.Repeat("\n", count[0]) + return s +} + +// PadBoth returns the String instance padded to the left and right sides of the given length. +func (s *String) PadBoth(length int, pad ...string) *String { + defaultPad := " " + if len(pad) > 0 { + defaultPad = pad[0] + } + short := maximum(0, length-s.Length()) + left := short / 2 + right := short/2 + short%2 + + s.value = Substr(strings.Repeat(defaultPad, left), 0, left) + s.value + Substr(strings.Repeat(defaultPad, right), 0, right) + + return s +} + +// PadLeft returns the String instance padded to the left side of the given length. +func (s *String) PadLeft(length int, pad ...string) *String { + defaultPad := " " + if len(pad) > 0 { + defaultPad = pad[0] + } + short := maximum(0, length-s.Length()) + + s.value = Substr(strings.Repeat(defaultPad, short), 0, short) + s.value + return s +} + +// PadRight returns the String instance padded to the right side of the given length. +func (s *String) PadRight(length int, pad ...string) *String { + defaultPad := " " + if len(pad) > 0 { + defaultPad = pad[0] + } + short := maximum(0, length-s.Length()) + + s.value = s.value + Substr(strings.Repeat(defaultPad, short), 0, short) + return s +} + +// Pipe passes the string to the given callback and returns the result. +func (s *String) Pipe(callback func(s string) string) *String { + s.value = callback(s.value) + return s +} + +// Prepend one or more strings to the current string. +func (s *String) Prepend(values ...string) *String { + s.value = strings.Join(values, "") + s.value + return s +} + +// Remove returns the String instance with the first occurrence of the given value removed. +func (s *String) Remove(values ...string) *String { + for _, value := range values { + s.value = strings.ReplaceAll(s.value, value, "") + } + + return s +} + +// Repeat returns the String instance repeated the given number of times. +func (s *String) Repeat(times int) *String { + s.value = strings.Repeat(s.value, times) + return s +} + +// Replace returns the String instance with all occurrences of the search string replaced by the given replacement string. +func (s *String) Replace(search string, replace string, caseSensitive ...bool) *String { + caseSensitive = append(caseSensitive, true) + if len(caseSensitive) > 0 && !caseSensitive[0] { + s.value = regexp.MustCompile("(?i)"+search).ReplaceAllString(s.value, replace) + return s + } + s.value = strings.ReplaceAll(s.value, search, replace) + return s +} + +// ReplaceEnd returns the String instance with the last occurrence of the given value replaced. +func (s *String) ReplaceEnd(search string, replace string) *String { + if search == "" { + return s + } + + if s.EndsWith(search) { + return s.ReplaceLast(search, replace) + } + + return s +} + +// ReplaceFirst returns the String instance with the first occurrence of the given value replaced. +func (s *String) ReplaceFirst(search string, replace string) *String { + if search == "" { + return s + } + s.value = strings.Replace(s.value, search, replace, 1) + return s +} + +// ReplaceLast returns the String instance with the last occurrence of the given value replaced. +func (s *String) ReplaceLast(search string, replace string) *String { + if search == "" { + return s + } + index := strings.LastIndex(s.value, search) + if index != -1 { + s.value = s.value[:index] + replace + s.value[index+len(search):] + return s + } + + return s +} + +// ReplaceMatches returns the String instance with all occurrences of the given pattern +// replaced by the given replacement string. +func (s *String) ReplaceMatches(pattern string, replace string) *String { + s.value = regexp.MustCompile(pattern).ReplaceAllString(s.value, replace) + return s +} + +// ReplaceStart returns the String instance with the first occurrence of the given value replaced. +func (s *String) ReplaceStart(search string, replace string) *String { + if search == "" { + return s + } + + if s.StartsWith(search) { + return s.ReplaceFirst(search, replace) + } + + return s +} + +// RTrim returns the String instance with the right occurrences of the given value removed. +func (s *String) RTrim(characters ...string) *String { + if len(characters) == 0 { + s.value = strings.TrimRight(s.value, " ") + return s + } + + s.value = strings.TrimRight(s.value, characters[0]) + return s +} + +// Snake returns the String instance in snake case. +func (s *String) Snake(delimiter ...string) *String { + defaultDelimiter := "_" + if len(delimiter) > 0 { + defaultDelimiter = delimiter[0] + } + words := fieldsFunc(s.value, func(r rune) bool { + return r == ' ' || r == ',' || r == '.' || r == '-' || r == '_' + }, func(r rune) bool { + return unicode.IsUpper(r) + }) + + casesLower := cases.Lower(language.Und) + var studlyWords []string + for _, word := range words { + studlyWords = append(studlyWords, casesLower.String(word)) + } + + s.value = strings.Join(studlyWords, defaultDelimiter) + return s +} + +// Split splits the string by given pattern string. +func (s *String) Split(pattern string, limit ...int) []string { + r := regexp.MustCompile(pattern) + defaultLimit := -1 + if len(limit) != 0 { + defaultLimit = limit[0] + } + + return r.Split(s.value, defaultLimit) +} + +// Squish returns the String instance with consecutive whitespace characters collapsed into a single space. +func (s *String) Squish() *String { + leadWhitespace := regexp.MustCompile(`^[\s\p{Zs}]+|[\s\p{Zs}]+$`) + insideWhitespace := regexp.MustCompile(`[\s\p{Zs}]{2,}`) + s.value = leadWhitespace.ReplaceAllString(s.value, "") + s.value = insideWhitespace.ReplaceAllString(s.value, " ") + return s +} + +// Start returns the String instance with the given value prepended. +func (s *String) Start(prefix string) *String { + quoted := regexp.QuoteMeta(prefix) + re := regexp.MustCompile(`^(` + quoted + `)+`) + s.value = prefix + re.ReplaceAllString(s.value, "") + return s +} + +// StartsWith returns true if the string starts with the given value or any of the values. +func (s *String) StartsWith(values ...string) bool { + for _, value := range values { + if strings.HasPrefix(s.value, value) { + return true + } + } + + return false +} + +// String returns the string value. +func (s *String) String() string { + return s.value +} + +// Studly returns the String instance in studly case. +func (s *String) Studly() *String { + words := fieldsFunc(s.value, func(r rune) bool { + return r == '_' || r == ' ' || r == '-' || r == ',' || r == '.' + }, func(r rune) bool { + return unicode.IsUpper(r) + }) + + casesTitle := cases.Title(language.Und) + var studlyWords []string + for _, word := range words { + studlyWords = append(studlyWords, casesTitle.String(word)) + } + + s.value = strings.Join(studlyWords, "") + return s +} + +// Substr returns the String instance starting at the given index with the specified length. +func (s *String) Substr(start int, length ...int) *String { + s.value = Substr(s.value, start, length...) + return s +} + +// Swap replaces all occurrences of the search string with the given replacement string. +func (s *String) Swap(replacements map[string]string) *String { + if len(replacements) == 0 { + return s + } + + oldNewPairs := make([]string, 0, len(replacements)*2) + for k, v := range replacements { + if k == "" { + return s + } + oldNewPairs = append(oldNewPairs, k, v) + } + + s.value = strings.NewReplacer(oldNewPairs...).Replace(s.value) + return s +} + +// Tap passes the string to the given callback and returns the string. +func (s *String) Tap(callback func(String)) *String { + callback(*s) + return s +} + +// Test returns true if the string matches the given pattern. +func (s *String) Test(pattern string) bool { + return s.IsMatch(pattern) +} + +// Title returns the String instance in title case. +func (s *String) Title() *String { + casesTitle := cases.Title(language.Und) + s.value = casesTitle.String(s.value) + return s +} + +// Trim returns the String instance with trimmed characters from the left and right sides. +func (s *String) Trim(characters ...string) *String { + if len(characters) == 0 { + s.value = strings.TrimSpace(s.value) + return s + } + + s.value = strings.Trim(s.value, characters[0]) + return s +} + +// UcFirst returns the String instance with the first character uppercased. +func (s *String) UcFirst() *String { + if s.Length() == 0 { + return s + } + s.value = strings.ToUpper(Substr(s.value, 0, 1)) + Substr(s.value, 1) + return s +} + +// UcSplit splits the string into words using uppercase characters as the delimiter. +func (s *String) UcSplit() []string { + words := fieldsFunc(s.value, func(r rune) bool { + return false + }, func(r rune) bool { + return unicode.IsUpper(r) + }) + return words +} + +// Unless returns the String instance with the given fallback applied if the given condition is false. +func (s *String) Unless(callback func(*String) bool, fallback func(*String) *String) *String { + if !callback(s) { + return fallback(s) + } + + return s +} + +// Upper returns the String instance in upper case. +func (s *String) Upper() *String { + s.value = strings.ToUpper(s.value) + return s +} + +// When returns the String instance with the given callback applied if the given condition is true. +// If the condition is false, the fallback callback is applied.(if provided) +func (s *String) When(condition bool, callback ...func(*String) *String) *String { + if condition { + return callback[0](s) + } else { + if len(callback) > 1 { + return callback[1](s) + } + } + + return s +} + +// WhenContains returns the String instance with the given callback applied if the string contains the given value. +func (s *String) WhenContains(value string, callback ...func(*String) *String) *String { + return s.When(s.Contains(value), callback...) +} + +// WhenContainsAll returns the String instance with the given callback applied if the string contains all the given values. +func (s *String) WhenContainsAll(values []string, callback ...func(*String) *String) *String { + return s.When(s.ContainsAll(values...), callback...) +} + +// WhenEmpty returns the String instance with the given callback applied if the string is empty. +func (s *String) WhenEmpty(callback ...func(*String) *String) *String { + return s.When(s.IsEmpty(), callback...) +} + +// WhenIsAscii returns the String instance with the given callback applied if the string contains only ASCII characters. +func (s *String) WhenIsAscii(callback ...func(*String) *String) *String { + return s.When(s.IsAscii(), callback...) +} + +// WhenNotEmpty returns the String instance with the given callback applied if the string is not empty. +func (s *String) WhenNotEmpty(callback ...func(*String) *String) *String { + return s.When(s.IsNotEmpty(), callback...) +} + +// WhenStartsWith returns the String instance with the given callback applied if the string starts with the given value. +func (s *String) WhenStartsWith(value []string, callback ...func(*String) *String) *String { + return s.When(s.StartsWith(value...), callback...) +} + +// WhenEndsWith returns the String instance with the given callback applied if the string ends with the given value. +func (s *String) WhenEndsWith(value []string, callback ...func(*String) *String) *String { + return s.When(s.EndsWith(value...), callback...) +} + +// WhenExactly returns the String instance with the given callback applied if the string is exactly the given value. +func (s *String) WhenExactly(value string, callback ...func(*String) *String) *String { + return s.When(s.Exactly(value), callback...) +} + +// WhenNotExactly returns the String instance with the given callback applied if the string is not exactly the given value. +func (s *String) WhenNotExactly(value string, callback ...func(*String) *String) *String { + return s.When(!s.Exactly(value), callback...) +} + +// WhenIs returns the String instance with the given callback applied if the string matches any of the given patterns. +func (s *String) WhenIs(value string, callback ...func(*String) *String) *String { + return s.When(s.Is(value), callback...) +} + +// WhenIsUlid returns the String instance with the given callback applied if the string is a valid ULID. +func (s *String) WhenIsUlid(callback ...func(*String) *String) *String { + return s.When(s.IsUlid(), callback...) +} + +// WhenIsUuid returns the String instance with the given callback applied if the string is a valid UUID. +func (s *String) WhenIsUuid(callback ...func(*String) *String) *String { + return s.When(s.IsUuid(), callback...) +} + +// WhenTest returns the String instance with the given callback applied if the string matches the given pattern. +func (s *String) WhenTest(pattern string, callback ...func(*String) *String) *String { + return s.When(s.Test(pattern), callback...) +} + +// WordCount returns the number of words in the string. +func (s *String) WordCount() int { + return len(strings.Fields(s.value)) +} + +// Words return the String instance truncated to the given number of words. +func (s *String) Words(limit int, end ...string) *String { + defaultEnd := "..." + if len(end) > 0 { + defaultEnd = end[0] + } + + words := strings.Fields(s.value) + if len(words) <= limit { + return s + } + + s.value = strings.Join(words[:limit], " ") + defaultEnd + return s +} + +// Substr returns a substring of a given string, starting at the specified index +// and with a specified length. +// It handles UTF-8 encoded strings. +func Substr(str string, start int, length ...int) string { + // Convert the string to a rune slice for proper handling of UTF-8 encoding. + runes := []rune(str) + strLen := utf8.RuneCountInString(str) + end := strLen + // Check if the start index is out of bounds. + if start >= strLen { + return "" + } + + // If the start index is negative, count backwards from the end of the string. + if start < 0 { + start = strLen + start + if start < 0 { + start = 0 + } + } + + if len(length) > 0 { + if length[0] >= 0 { + end = start + length[0] + } else { + end = strLen + length[0] + } + } + + // If the length is 0, return the substring from start to the end of the string. + if len(length) == 0 { + return string(runes[start:]) + } + + // Handle the case where lenArg is negative and less than start + if end < start { + return "" + } + + if end > strLen { + end = strLen + } + + // Return the substring. + return string(runes[start:end]) +} + func Random(length int) string { b := make([]byte, length) _, err := rand.Read(b) @@ -91,3 +1007,50 @@ func (b *Buffer) append(s string) *Buffer { return b } + +// fieldsFunc splits the input string into words with preservation, following the rules defined by +// the provided functions f and preserveFunc. +func fieldsFunc(s string, f func(rune) bool, preserveFunc ...func(rune) bool) []string { + var fields []string + var currentField strings.Builder + + shouldPreserve := func(r rune) bool { + for _, preserveFn := range preserveFunc { + if preserveFn(r) { + return true + } + } + return false + } + + for _, r := range s { + if f(r) { + if currentField.Len() > 0 { + fields = append(fields, currentField.String()) + currentField.Reset() + } + } else if shouldPreserve(r) { + if currentField.Len() > 0 { + fields = append(fields, currentField.String()) + currentField.Reset() + } + currentField.WriteRune(r) + } else { + currentField.WriteRune(r) + } + } + + if currentField.Len() > 0 { + fields = append(fields, currentField.String()) + } + + return fields +} + +// maximum returns the largest of x or y. +func maximum[T constraints.Ordered](x T, y T) T { + if x > y { + return x + } + return y +} diff --git a/support/str/str_test.go b/support/str/str_test.go index 6c4a83cc6..87742960b 100644 --- a/support/str/str_test.go +++ b/support/str/str_test.go @@ -1,14 +1,1099 @@ package str import ( + "runtime" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) +type StringTestSuite struct { + suite.Suite +} + +func TestStringTestSuite(t *testing.T) { + suite.Run(t, &StringTestSuite{}) +} + +func (s *StringTestSuite) SetupTest() { +} + +func (s *StringTestSuite) TestAfter() { + s.Equal("Framework", Of("GoravelFramework").After("Goravel").String()) + s.Equal("lel", Of("parallel").After("l").String()) + s.Equal("3def", Of("abc123def").After("2").String()) + s.Equal("abc123def", Of("abc123def").After("4").String()) + s.Equal("GoravelFramework", Of("GoravelFramework").After("").String()) +} + +func (s *StringTestSuite) TestAfterLast() { + s.Equal("Framework", Of("GoravelFramework").AfterLast("Goravel").String()) + s.Equal("", Of("parallel").AfterLast("l").String()) + s.Equal("3def", Of("abc123def").AfterLast("2").String()) + s.Equal("abc123def", Of("abc123def").AfterLast("4").String()) +} + +func (s *StringTestSuite) TestAppend() { + s.Equal("foobar", Of("foo").Append("bar").String()) + s.Equal("foobar", Of("foo").Append("bar").Append("").String()) + s.Equal("foobar", Of("foo").Append("bar").Append().String()) +} + +func (s *StringTestSuite) TestBasename() { + s.Equal("str", Of("/framework/support/str").Basename().String()) + s.Equal("str", Of("/framework/support/str/").Basename().String()) + s.Equal("str", Of("str").Basename().String()) + s.Equal("str", Of("/str").Basename().String()) + s.Equal("str", Of("/str/").Basename().String()) + s.Equal("str", Of("str/").Basename().String()) + + str := Of("/").Basename().String() + if runtime.GOOS == "windows" { + s.Equal("\\", str) + } else { + s.Equal("/", str) + } + + s.Equal(".", Of("").Basename().String()) + s.Equal("str", Of("/framework/support/str/str.go").Basename(".go").String()) +} + +func (s *StringTestSuite) TestBefore() { + s.Equal("Goravel", Of("GoravelFramework").Before("Framework").String()) + s.Equal("para", Of("parallel").Before("l").String()) + s.Equal("abc123", Of("abc123def").Before("def").String()) + s.Equal("abc", Of("abc123def").Before("123").String()) +} + +func (s *StringTestSuite) TestBeforeLast() { + s.Equal("Goravel", Of("GoravelFramework").BeforeLast("Framework").String()) + s.Equal("paralle", Of("parallel").BeforeLast("l").String()) + s.Equal("abc123", Of("abc123def").BeforeLast("def").String()) + s.Equal("abc", Of("abc123def").BeforeLast("123").String()) +} + +func (s *StringTestSuite) TestBetween() { + s.Equal("foobarbaz", Of("foobarbaz").Between("", "b").String()) + s.Equal("foobarbaz", Of("foobarbaz").Between("f", "").String()) + s.Equal("foobarbaz", Of("foobarbaz").Between("", "").String()) + s.Equal("obar", Of("foobarbaz").Between("o", "b").String()) + s.Equal("bar", Of("foobarbaz").Between("foo", "baz").String()) + s.Equal("foo][bar][baz", Of("[foo][bar][baz]").Between("[", "]").String()) +} + +func (s *StringTestSuite) TestBetweenFirst() { + s.Equal("foobarbaz", Of("foobarbaz").BetweenFirst("", "b").String()) + s.Equal("foobarbaz", Of("foobarbaz").BetweenFirst("f", "").String()) + s.Equal("foobarbaz", Of("foobarbaz").BetweenFirst("", "").String()) + s.Equal("o", Of("foobarbaz").BetweenFirst("o", "b").String()) + s.Equal("foo", Of("[foo][bar][baz]").BetweenFirst("[", "]").String()) + s.Equal("foobar", Of("foofoobarbaz").BetweenFirst("foo", "baz").String()) +} + +func (s *StringTestSuite) TestCamel() { + s.Equal("goravelGOFramework", Of("Goravel_g_o_framework").Camel().String()) + s.Equal("goravelGOFramework", Of("Goravel_gO_framework").Camel().String()) + s.Equal("goravelGoFramework", Of("Goravel -_- go -_- framework ").Camel().String()) + + s.Equal("fooBar", Of("FooBar").Camel().String()) + s.Equal("fooBar", Of("foo_bar").Camel().String()) + s.Equal("fooBar", Of("foo-Bar").Camel().String()) + s.Equal("fooBar", Of("foo bar").Camel().String()) + s.Equal("fooBar", Of("foo.bar").Camel().String()) +} + +func (s *StringTestSuite) TestCharAt() { + s.Equal("好", Of("你好,世界!").CharAt(1)) + s.Equal("त", Of("नमस्ते, दुनिया!").CharAt(4)) + s.Equal("w", Of("Привет, world!").CharAt(8)) + s.Equal("계", Of("안녕하세요, 세계!").CharAt(-2)) + s.Equal("", Of("こんにちは、世界!").CharAt(-200)) +} + +func (s *StringTestSuite) TestContains() { + s.True(Of("kkumar").Contains("uma")) + s.True(Of("kkumar").Contains("kumar")) + s.True(Of("kkumar").Contains("uma", "xyz")) + s.False(Of("kkumar").Contains("xyz")) + s.False(Of("kkumar").Contains("")) +} + +func (s *StringTestSuite) TestContainsAll() { + s.True(Of("krishan kumar").ContainsAll("krishan", "kumar")) + s.True(Of("krishan kumar").ContainsAll("kumar")) + s.False(Of("krishan kumar").ContainsAll("kumar", "xyz")) +} + +func (s *StringTestSuite) TestDirname() { + str := Of("/framework/support/str").Dirname().String() + if runtime.GOOS == "windows" { + s.Equal("\\framework\\support", str) + } else { + s.Equal("/framework/support", str) + } + + str = Of("/framework/support/str").Dirname(2).String() + if runtime.GOOS == "windows" { + s.Equal("\\framework", str) + } else { + s.Equal("/framework", str) + } + + s.Equal(".", Of("framework").Dirname().String()) + s.Equal(".", Of(".").Dirname().String()) + + str = Of("/").Dirname().String() + if runtime.GOOS == "windows" { + s.Equal("\\", str) + } else { + s.Equal("/", str) + } + + str = Of("/framework/").Dirname(2).String() + if runtime.GOOS == "windows" { + s.Equal("\\", str) + } else { + s.Equal("/", str) + } +} + +func (s *StringTestSuite) TestEndsWith() { + s.True(Of("bowen").EndsWith("wen")) + s.True(Of("bowen").EndsWith("bowen")) + s.True(Of("bowen").EndsWith("wen", "xyz")) + s.False(Of("bowen").EndsWith("xyz")) + s.False(Of("bowen").EndsWith("")) + s.False(Of("bowen").EndsWith()) + s.False(Of("bowen").EndsWith("N")) + s.True(Of("a7.12").EndsWith("7.12")) + // Test for muti-byte string + s.True(Of("你好").EndsWith("好")) + s.True(Of("你好").EndsWith("你好")) + s.True(Of("你好").EndsWith("好", "xyz")) + s.False(Of("你好").EndsWith("xyz")) + s.False(Of("你好").EndsWith("")) +} + +func (s *StringTestSuite) TestExactly() { + s.True(Of("foo").Exactly("foo")) + s.False(Of("foo").Exactly("Foo")) +} + +func (s *StringTestSuite) TestExcerpt() { + s.Equal("...is a beautiful morn...", Of("This is a beautiful morning").Excerpt("beautiful", ExcerptOption{ + Radius: 5, + }).String()) + s.Equal("This is a beautiful morning", Of("This is a beautiful morning").Excerpt("foo", ExcerptOption{ + Radius: 5, + }).String()) + s.Equal("(...)is a beautiful morn(...)", Of("This is a beautiful morning").Excerpt("beautiful", ExcerptOption{ + Omission: "(...)", + Radius: 5, + }).String()) +} + +func (s *StringTestSuite) TestExplode() { + s.Equal([]string{"Foo", "Bar", "Baz"}, Of("Foo Bar Baz").Explode(" ")) + // with limit + s.Equal([]string{"Foo", "Bar Baz"}, Of("Foo Bar Baz").Explode(" ", 2)) + s.Equal([]string{"Foo", "Bar"}, Of("Foo Bar Baz").Explode(" ", -1)) + s.Equal([]string{}, Of("Foo Bar Baz").Explode(" ", -10)) +} + +func (s *StringTestSuite) TestFinish() { + s.Equal("abbc", Of("ab").Finish("bc").String()) + s.Equal("abbc", Of("abbcbc").Finish("bc").String()) + s.Equal("abcbbc", Of("abcbbcbc").Finish("bc").String()) +} + +func (s *StringTestSuite) TestHeadline() { + s.Equal("Hello", Of("hello").Headline().String()) + s.Equal("This Is A Headline", Of("this is a headline").Headline().String()) + s.Equal("Camelcase Is A Headline", Of("CamelCase is a headline").Headline().String()) + s.Equal("Kebab-Case Is A Headline", Of("kebab-case is a headline").Headline().String()) +} + +func (s *StringTestSuite) TestIs() { + s.True(Of("foo").Is("foo", "bar", "baz")) + s.True(Of("foo123").Is("bar*", "baz*", "foo*")) + s.False(Of("foo").Is("bar", "baz")) + s.True(Of("a.b").Is("a.b", "c.*")) + s.False(Of("abc*").Is("abc\\*", "xyz*")) + s.False(Of("").Is("foo")) + s.True(Of("foo/bar/baz").Is("foo/*", "bar/*", "baz*")) + // Is case-sensitive + s.False(Of("foo/bar/baz").Is("*BAZ*")) +} + +func (s *StringTestSuite) TestIsEmpty() { + s.True(Of("").IsEmpty()) + s.False(Of("F").IsEmpty()) +} + +func (s *StringTestSuite) TestIsNotEmpty() { + s.False(Of("").IsNotEmpty()) + s.True(Of("F").IsNotEmpty()) +} + +func (s *StringTestSuite) TestIsAscii() { + s.True(Of("abc").IsAscii()) + s.False(Of("你好").IsAscii()) +} + +func (s *StringTestSuite) TestIsSlice() { + // Test when the string represents a valid JSON array + s.True(Of(`["apple", "banana", "cherry"]`).IsSlice()) + + // Test when the string represents a valid JSON array with objects + s.True(Of(`[{"name": "John"}, {"name": "Alice"}]`).IsSlice()) + + // Test when the string represents an empty JSON array + s.True(Of(`[]`).IsSlice()) + + // Test when the string represents an invalid JSON object + s.False(Of(`{"name": "John"}`).IsSlice()) + + // Test when the string is not valid JSON + s.False(Of(`Not a JSON array`).IsSlice()) + + // Test when the string is empty + s.False(Of("").IsSlice()) +} + +func (s *StringTestSuite) TestIsMap() { + // Test when the string represents a valid JSON object + s.True(Of(`{"name": "John", "age": 30}`).IsMap()) + + // Test when the string represents a valid JSON object with nested objects + s.True(Of(`{"person": {"name": "Alice", "age": 25}}`).IsMap()) + + // Test when the string represents an empty JSON object + s.True(Of(`{}`).IsMap()) + + // Test when the string represents an invalid JSON array + s.False(Of(`["apple", "banana", "cherry"]`).IsMap()) + + // Test when the string is not valid JSON + s.False(Of(`Not a JSON object`).IsMap()) + + // Test when the string is empty + s.False(Of("").IsMap()) +} + +func (s *StringTestSuite) TestIsUlid() { + s.True(Of("01E65Z7XCHCR7X1P2MKF78ENRP").IsUlid()) + // lowercase characters are not allowed + s.False(Of("01e65z7xchcr7x1p2mkf78enrp").IsUlid()) + // too short (ULIDS must be 26 characters long) + s.False(Of("01E65Z7XCHCR7X1P2MKF78E").IsUlid()) + // contains invalid characters + s.False(Of("01E65Z7XCHCR7X1P2MKF78ENR!").IsUlid()) +} + +func (s *StringTestSuite) TestIsUuid() { + s.True(Of("3f2504e0-4f89-41d3-9a0c-0305e82c3301").IsUuid()) + s.False(Of("3f2504e0-4f89-41d3-9a0c-0305e82c3301-extra").IsUuid()) +} + +func (s *StringTestSuite) TestKebab() { + s.Equal("goravel-framework", Of("GoravelFramework").Kebab().String()) +} + +func (s *StringTestSuite) TestLcFirst() { + s.Equal("framework", Of("Framework").LcFirst().String()) + s.Equal("framework", Of("framework").LcFirst().String()) +} + +func (s *StringTestSuite) TestLength() { + s.Equal(11, Of("foo bar baz").Length()) + s.Equal(0, Of("").Length()) +} + +func (s *StringTestSuite) TestLimit() { + s.Equal("This is...", Of("This is a beautiful morning").Limit(7).String()) + s.Equal("This is****", Of("This is a beautiful morning").Limit(7, "****").String()) + s.Equal("这是一...", Of("这是一段中文").Limit(3).String()) + s.Equal("这是一段中文", Of("这是一段中文").Limit(9).String()) +} + +func (s *StringTestSuite) TestLower() { + s.Equal("foo bar baz", Of("FOO BAR BAZ").Lower().String()) + s.Equal("foo bar baz", Of("fOo Bar bAz").Lower().String()) +} + +func (s *StringTestSuite) TestLTrim() { + s.Equal("foo ", Of(" foo ").LTrim().String()) +} + +func (s *StringTestSuite) TestMask() { + s.Equal("kri**************", Of("krishan@email.com").Mask("*", 3).String()) + s.Equal("*******@email.com", Of("krishan@email.com").Mask("*", 0, 7).String()) + s.Equal("kris*************", Of("krishan@email.com").Mask("*", -13).String()) + s.Equal("kris***@email.com", Of("krishan@email.com").Mask("*", -13, 3).String()) + + s.Equal("*****************", Of("krishan@email.com").Mask("*", -17).String()) + s.Equal("*****an@email.com", Of("krishan@email.com").Mask("*", -99, 5).String()) + + s.Equal("krishan@email.com", Of("krishan@email.com").Mask("*", 17).String()) + s.Equal("krishan@email.com", Of("krishan@email.com").Mask("*", 17, 99).String()) + + s.Equal("krishan@email.com", Of("krishan@email.com").Mask("", 3).String()) + + s.Equal("krissssssssssssss", Of("krishan@email.com").Mask("something", 3).String()) + + s.Equal("这是一***", Of("这是一段中文").Mask("*", 3).String()) + s.Equal("**一段中文", Of("这是一段中文").Mask("*", 0, 2).String()) +} + +func (s *StringTestSuite) TestMatch() { + s.Equal("World", Of("Hello, World!").Match("World").String()) + s.Equal("(test)", Of("This is a (test) string").Match(`\([^)]+\)`).String()) + s.Equal("123", Of("abc123def456def").Match(`\d+`).String()) + s.Equal("", Of("No match here").Match(`\d+`).String()) + s.Equal("Hello, World!", Of("Hello, World!").Match("").String()) + s.Equal("[456]", Of("123 [456]").Match(`\[456\]`).String()) +} + +func (s *StringTestSuite) TestMatchAll() { + s.Equal([]string{"World"}, Of("Hello, World!").MatchAll("World")) + s.Equal([]string{"(test)"}, Of("This is a (test) string").MatchAll(`\([^)]+\)`)) + s.Equal([]string{"123", "456"}, Of("abc123def456def").MatchAll(`\d+`)) + s.Equal([]string(nil), Of("No match here").MatchAll(`\d+`)) + s.Equal([]string{"Hello, World!"}, Of("Hello, World!").MatchAll("")) + s.Equal([]string{"[456]"}, Of("123 [456]").MatchAll(`\[456\]`)) +} + +func (s *StringTestSuite) TestIsMatch() { + // Test matching with a single pattern + s.True(Of("Hello, Goravel!").IsMatch(`.*,.*!`)) + s.True(Of("Hello, Goravel!").IsMatch(`^.*$(.*)`)) + s.True(Of("Hello, Goravel!").IsMatch(`(?i)goravel`)) + s.True(Of("Hello, GOravel!").IsMatch(`^(.*(.*(.*)))`)) + + // Test non-matching with a single pattern + s.False(Of("Hello, Goravel!").IsMatch(`H.o`)) + s.False(Of("Hello, Goravel!").IsMatch(`^goravel!`)) + s.False(Of("Hello, Goravel!").IsMatch(`goravel!(.*)`)) + s.False(Of("Hello, Goravel!").IsMatch(`^[a-zA-Z,!]+$`)) + + // Test with multiple patterns + s.True(Of("Hello, Goravel!").IsMatch(`.*,.*!`, `H.o`)) + s.True(Of("Hello, Goravel!").IsMatch(`(?i)goravel`, `^.*$(.*)`)) + s.True(Of("Hello, Goravel!").IsMatch(`(?i)goravel`, `goravel!(.*)`)) + s.True(Of("Hello, Goravel!").IsMatch(`^[a-zA-Z,!]+$`, `^(.*(.*(.*)))`)) +} + +func (s *StringTestSuite) TestNewLine() { + s.Equal("Goravel\n", Of("Goravel").NewLine().String()) + s.Equal("Goravel\n\nbar", Of("Goravel").NewLine(2).Append("bar").String()) +} + +func (s *StringTestSuite) TestPadBoth() { + // Test padding with spaces + s.Equal(" Hello ", Of("Hello").PadBoth(11, " ").String()) + s.Equal(" World! ", Of("World!").PadBoth(10, " ").String()) + s.Equal("==Hello===", Of("Hello").PadBoth(10, "=").String()) + s.Equal("Hello", Of("Hello").PadBoth(3, " ").String()) + s.Equal(" ", Of("").PadBoth(6, " ").String()) +} + +func (s *StringTestSuite) TestPadLeft() { + s.Equal(" Goravel", Of("Goravel").PadLeft(10, " ").String()) + s.Equal("==Goravel", Of("Goravel").PadLeft(9, "=").String()) + s.Equal("Goravel", Of("Goravel").PadLeft(3, " ").String()) +} + +func (s *StringTestSuite) TestPadRight() { + s.Equal("Goravel ", Of("Goravel").PadRight(10, " ").String()) + s.Equal("Goravel==", Of("Goravel").PadRight(9, "=").String()) + s.Equal("Goravel", Of("Goravel").PadRight(3, " ").String()) +} + +func (s *StringTestSuite) TestPipe() { + callback := func(str string) string { + return Of(str).Append("bar").String() + } + s.Equal("foobar", Of("foo").Pipe(callback).String()) +} + +func (s *StringTestSuite) TestPrepend() { + s.Equal("foobar", Of("bar").Prepend("foo").String()) + s.Equal("foobar", Of("bar").Prepend("foo").Prepend("").String()) + s.Equal("foobar", Of("bar").Prepend("foo").Prepend().String()) +} + +func (s *StringTestSuite) TestRemove() { + s.Equal("Fbar", Of("Foobar").Remove("o").String()) + s.Equal("Foo", Of("Foobar").Remove("bar").String()) + s.Equal("oobar", Of("Foobar").Remove("F").String()) + s.Equal("Foobar", Of("Foobar").Remove("f").String()) + + s.Equal("Fbr", Of("Foobar").Remove("o", "a").String()) + s.Equal("Fooar", Of("Foobar").Remove("f", "b").String()) + s.Equal("Foobar", Of("Foo|bar").Remove("f", "|").String()) +} + +func (s *StringTestSuite) TestRepeat() { + s.Equal("aaaaa", Of("a").Repeat(5).String()) + s.Equal("", Of("").Repeat(5).String()) +} + +func (s *StringTestSuite) TestReplace() { + s.Equal("foo/foo/foo", Of("?/?/?").Replace("?", "foo").String()) + s.Equal("foo/foo/foo", Of("x/x/x").Replace("X", "foo", false).String()) + s.Equal("bar/bar", Of("?/?").Replace("?", "bar").String()) + s.Equal("?/?/?", Of("? ? ?").Replace(" ", "/").String()) +} + +func (s *StringTestSuite) TestReplaceEnd() { + s.Equal("Golang is great!", Of("Golang is good!").ReplaceEnd("good!", "great!").String()) + s.Equal("Hello, World!", Of("Hello, Earth!").ReplaceEnd("Earth!", "World!").String()) + s.Equal("München Berlin", Of("München Frankfurt").ReplaceEnd("Frankfurt", "Berlin").String()) + s.Equal("Café Latte", Of("Café Americano").ReplaceEnd("Americano", "Latte").String()) + s.Equal("Golang is good!", Of("Golang is good!").ReplaceEnd("", "great!").String()) + s.Equal("Golang is good!", Of("Golang is good!").ReplaceEnd("excellent!", "great!").String()) +} + +func (s *StringTestSuite) TestReplaceFirst() { + s.Equal("fooqux foobar", Of("foobar foobar").ReplaceFirst("bar", "qux").String()) + s.Equal("foo/qux? foo/bar?", Of("foo/bar? foo/bar?").ReplaceFirst("bar?", "qux?").String()) + s.Equal("foo foobar", Of("foobar foobar").ReplaceFirst("bar", "").String()) + s.Equal("foobar foobar", Of("foobar foobar").ReplaceFirst("xxx", "yyy").String()) + s.Equal("foobar foobar", Of("foobar foobar").ReplaceFirst("", "yyy").String()) + // Test for multibyte string support + s.Equal("Jxxxnköping Malmö", Of("Jönköping Malmö").ReplaceFirst("ö", "xxx").String()) + s.Equal("Jönköping Malmö", Of("Jönköping Malmö").ReplaceFirst("", "yyy").String()) +} + +func (s *StringTestSuite) TestReplaceLast() { + s.Equal("foobar fooqux", Of("foobar foobar").ReplaceLast("bar", "qux").String()) + s.Equal("foo/bar? foo/qux?", Of("foo/bar? foo/bar?").ReplaceLast("bar?", "qux?").String()) + s.Equal("foobar foo", Of("foobar foobar").ReplaceLast("bar", "").String()) + s.Equal("foobar foobar", Of("foobar foobar").ReplaceLast("xxx", "yyy").String()) + s.Equal("foobar foobar", Of("foobar foobar").ReplaceLast("", "yyy").String()) + // Test for multibyte string support + s.Equal("Malmö Jönkxxxping", Of("Malmö Jönköping").ReplaceLast("ö", "xxx").String()) + s.Equal("Malmö Jönköping", Of("Malmö Jönköping").ReplaceLast("", "yyy").String()) +} + +func (s *StringTestSuite) TestReplaceMatches() { + s.Equal("Golang is great!", Of("Golang is good!").ReplaceMatches("good", "great").String()) + s.Equal("Hello, World!", Of("Hello, Earth!").ReplaceMatches("Earth", "World").String()) + s.Equal("Apples, Apples, Apples", Of("Oranges, Oranges, Oranges").ReplaceMatches("Oranges", "Apples").String()) + s.Equal("1, 2, 3, 4, 5", Of("10, 20, 30, 40, 50").ReplaceMatches("0", "").String()) + s.Equal("München Berlin", Of("München Frankfurt").ReplaceMatches("Frankfurt", "Berlin").String()) + s.Equal("Café Latte", Of("Café Americano").ReplaceMatches("Americano", "Latte").String()) + s.Equal("The quick brown fox", Of("The quick brown fox").ReplaceMatches(`\b([a-z])`, `$1`).String()) + s.Equal("One, One, One", Of("1, 2, 3").ReplaceMatches(`\d`, "One").String()) + s.Equal("Hello, World!", Of("Hello, World!").ReplaceMatches("Earth", "").String()) + s.Equal("Hello, World!", Of("Hello, World!").ReplaceMatches("Golang", "Great").String()) +} + +func (s *StringTestSuite) TestReplaceStart() { + s.Equal("foobar foobar", Of("foobar foobar").ReplaceStart("bar", "qux").String()) + s.Equal("foo/bar? foo/bar?", Of("foo/bar? foo/bar?").ReplaceStart("bar?", "qux?").String()) + s.Equal("quxbar foobar", Of("foobar foobar").ReplaceStart("foo", "qux").String()) + s.Equal("qux? foo/bar?", Of("foo/bar? foo/bar?").ReplaceStart("foo/bar?", "qux?").String()) + s.Equal("bar foobar", Of("foobar foobar").ReplaceStart("foo", "").String()) + s.Equal("1", Of("0").ReplaceStart("0", "1").String()) + // Test for multibyte string support + s.Equal("xxxnköping Malmö", Of("Jönköping Malmö").ReplaceStart("Jö", "xxx").String()) + s.Equal("Jönköping Malmö", Of("Jönköping Malmö").ReplaceStart("", "yyy").String()) +} + +func (s *StringTestSuite) TestRTrim() { + s.Equal(" foo", Of(" foo ").RTrim().String()) + s.Equal(" foo", Of(" foo__").RTrim("_").String()) +} + +func (s *StringTestSuite) TestSnake() { + s.Equal("goravel_g_o_framework", Of("GoravelGOFramework").Snake().String()) + s.Equal("goravel_go_framework", Of("GoravelGoFramework").Snake().String()) + s.Equal("goravel go framework", Of("GoravelGoFramework").Snake(" ").String()) + s.Equal("goravel_go_framework", Of("Goravel Go Framework").Snake().String()) + s.Equal("goravel_go_framework", Of("Goravel Go Framework ").Snake().String()) + s.Equal("goravel__go__framework", Of("GoravelGoFramework").Snake("__").String()) + s.Equal("żółta_łódka", Of("ŻółtaŁódka").Snake().String()) +} + +func (s *StringTestSuite) TestSplit() { + s.Equal([]string{"one", "two", "three", "four"}, Of("one-two-three-four").Split("-")) + s.Equal([]string{"", "", "D", "E", "", ""}, Of(",,D,E,,").Split(",")) + s.Equal([]string{"one", "two", "three,four"}, Of("one,two,three,four").Split(",", 3)) +} + +func (s *StringTestSuite) TestSquish() { + s.Equal("Hello World", Of(" Hello World ").Squish().String()) + s.Equal("A B C", Of("A B C").Squish().String()) + s.Equal("Lorem ipsum dolor sit amet", Of(" Lorem ipsum \n dolor sit \t amet ").Squish().String()) + s.Equal("Leading and trailing spaces", Of(" Leading "+ + "and trailing "+ + " spaces ").Squish().String()) + s.Equal("", Of("").Squish().String()) +} + +func (s *StringTestSuite) TestStart() { + s.Equal("/test/string", Of("test/string").Start("/").String()) + s.Equal("/test/string", Of("/test/string").Start("/").String()) + s.Equal("/test/string", Of("//test/string").Start("/").String()) +} + +func (s *StringTestSuite) TestStartsWith() { + s.True(Of("Wenbo Han").StartsWith("Wen")) + s.True(Of("Wenbo Han").StartsWith("Wenbo")) + s.True(Of("Wenbo Han").StartsWith("Han", "Wen")) + s.False(Of("Wenbo Han").StartsWith()) + s.False(Of("Wenbo Han").StartsWith("we")) + s.True(Of("Jönköping").StartsWith("Jö")) + s.False(Of("Jönköping").StartsWith("Jonko")) +} + +func (s *StringTestSuite) TestStudly() { + s.Equal("GoravelGOFramework", Of("Goravel_g_o_framework").Studly().String()) + s.Equal("GoravelGOFramework", Of("Goravel_gO_framework").Studly().String()) + s.Equal("GoravelGoFramework", Of("Goravel -_- go -_- framework ").Studly().String()) + + s.Equal("FooBar", Of("FooBar").Studly().String()) + s.Equal("FooBar", Of("foo_bar").Studly().String()) + s.Equal("FooBar", Of("foo-Bar").Studly().String()) + s.Equal("FooBar", Of("foo bar").Studly().String()) + s.Equal("FooBar", Of("foo.bar").Studly().String()) +} + +func (s *StringTestSuite) TestSubstr() { + s.Equal("Ё", Of("БГДЖИЛЁ").Substr(-1).String()) + s.Equal("ЛЁ", Of("БГДЖИЛЁ").Substr(-2).String()) + s.Equal("И", Of("БГДЖИЛЁ").Substr(-3, 1).String()) + s.Equal("ДЖИЛ", Of("БГДЖИЛЁ").Substr(2, -1).String()) + s.Equal("", Of("БГДЖИЛЁ").Substr(4, -4).String()) + s.Equal("ИЛ", Of("БГДЖИЛЁ").Substr(-3, -1).String()) + s.Equal("ГДЖИЛЁ", Of("БГДЖИЛЁ").Substr(1).String()) + s.Equal("ГДЖ", Of("БГДЖИЛЁ").Substr(1, 3).String()) + s.Equal("БГДЖ", Of("БГДЖИЛЁ").Substr(0, 4).String()) + s.Equal("Ё", Of("БГДЖИЛЁ").Substr(-1, 1).String()) + s.Equal("", Of("Б").Substr(2).String()) +} + +func (s *StringTestSuite) TestSwap() { + s.Equal("Go is excellent", Of("Golang is awesome").Swap(map[string]string{ + "Golang": "Go", + "awesome": "excellent", + }).String()) + s.Equal("Golang is awesome", Of("Golang is awesome").Swap(map[string]string{}).String()) + s.Equal("Golang is awesome", Of("Golang is awesome").Swap(map[string]string{ + "": "Go", + "awesome": "excellent", + }).String()) +} + +func (s *StringTestSuite) TestTap() { + tap := Of("foobarbaz") + fromTehTap := "" + tap = tap.Tap(func(s String) { + fromTehTap = s.Substr(0, 3).String() + }) + s.Equal("foo", fromTehTap) + s.Equal("foobarbaz", tap.String()) +} + +func (s *StringTestSuite) TestTitle() { + s.Equal("Krishan Kumar", Of("krishan kumar").Title().String()) + s.Equal("Krishan Kumar", Of("kriSHan kuMAr").Title().String()) +} + +func (s *StringTestSuite) TestTrim() { + s.Equal("foo", Of(" foo ").Trim().String()) + s.Equal("foo", Of("_foo_").Trim("_").String()) +} + +func (s *StringTestSuite) TestUcFirst() { + s.Equal("", Of("").UcFirst().String()) + s.Equal("Framework", Of("framework").UcFirst().String()) + s.Equal("Framework", Of("Framework").UcFirst().String()) + s.Equal(" framework", Of(" framework").UcFirst().String()) + s.Equal("Goravel framework", Of("goravel framework").UcFirst().String()) +} + +func (s *StringTestSuite) TestUcSplit() { + s.Equal([]string{"Krishan", "Kumar"}, Of("KrishanKumar").UcSplit()) + s.Equal([]string{"Hello", "From", "Goravel"}, Of("HelloFromGoravel").UcSplit()) + s.Equal([]string{"He_llo_", "World"}, Of("He_llo_World").UcSplit()) +} + +func (s *StringTestSuite) TestUnless() { + str := Of("Hello, World!") + + // Test case 1: The callback returns true, so the fallback should not be applied + s.Equal("Hello, World!", str.Unless(func(s *String) bool { + return true + }, func(s *String) *String { + return Of("This should not be applied") + }).String()) + + // Test case 2: The callback returns false, so the fallback should be applied + s.Equal("Fallback Applied", str.Unless(func(s *String) bool { + return false + }, func(s *String) *String { + return Of("Fallback Applied") + }).String()) + + // Test case 3: Testing with an empty string + s.Equal("Fallback Applied", Of("").Unless(func(s *String) bool { + return false + }, func(s *String) *String { + return Of("Fallback Applied") + }).String()) +} + +func (s *StringTestSuite) TestUpper() { + s.Equal("FOO BAR BAZ", Of("foo bar baz").Upper().String()) + s.Equal("FOO BAR BAZ", Of("foO bAr BaZ").Upper().String()) +} + +func (s *StringTestSuite) TestWhen() { + // true + s.Equal("when true", Of("when ").When(true, func(s *String) *String { + return s.Append("true") + }).String()) + s.Equal("gets a value from if", Of("gets a value ").When(true, func(s *String) *String { + return s.Append("from if") + }).String()) + + // false + s.Equal("when", Of("when").When(false, func(s *String) *String { + return s.Append("true") + }).String()) + + s.Equal("when false fallbacks to default", Of("when false ").When(false, func(s *String) *String { + return s.Append("true") + }, func(s *String) *String { + return s.Append("fallbacks to default") + }).String()) +} + +func (s *StringTestSuite) TestWhenContains() { + s.Equal("Tony Stark", Of("stark").WhenContains("tar", func(s *String) *String { + return s.Prepend("Tony ").Title() + }, func(s *String) *String { + return s.Prepend("Arno ").Title() + }).String()) + + s.Equal("stark", Of("stark").WhenContains("xxx", func(s *String) *String { + return s.Prepend("Tony ").Title() + }).String()) + + s.Equal("Arno Stark", Of("stark").WhenContains("xxx", func(s *String) *String { + return s.Prepend("Tony ").Title() + }, func(s *String) *String { + return s.Prepend("Arno ").Title() + }).String()) +} + +func (s *StringTestSuite) TestWhenContainsAll() { + // Test when all values are present + s.Equal("Tony Stark", Of("tony stark").WhenContainsAll([]string{"tony", "stark"}, + func(s *String) *String { + return s.Title() + }, + func(s *String) *String { + return s.Studly() + }, + ).String()) + + // Test when not all values are present + s.Equal("tony stark", Of("tony stark").WhenContainsAll([]string{"xxx"}, + func(s *String) *String { + return s.Title() + }, + ).String()) + + // Test when some values are present and some are not + s.Equal("TonyStark", Of("tony stark").WhenContainsAll([]string{"tony", "xxx"}, + func(s *String) *String { + return s.Title() + }, + func(s *String) *String { + return s.Studly() + }, + ).String()) +} + +func (s *StringTestSuite) TestWhenEmpty() { + // Test when the string is empty + s.Equal("DEFAULT", Of("").WhenEmpty( + func(s *String) *String { + return s.Append("default").Upper() + }).String()) + + // Test when the string is not empty + s.Equal("non-empty", Of("non-empty").WhenEmpty( + func(s *String) *String { + return s.Append("default") + }, + ).String()) +} + +func (s *StringTestSuite) TestWhenIsAscii() { + s.Equal("Ascii: A", Of("A").WhenIsAscii( + func(s *String) *String { + return s.Prepend("Ascii: ") + }).String()) + s.Equal("ù", Of("ù").WhenIsAscii( + func(s *String) *String { + return s.Prepend("Ascii: ") + }).String()) + s.Equal("Not Ascii: ù", Of("ù").WhenIsAscii( + func(s *String) *String { + return s.Prepend("Ascii: ") + }, + func(s *String) *String { + return s.Prepend("Not Ascii: ") + }, + ).String()) +} + +func (s *StringTestSuite) TestWhenNotEmpty() { + // Test when the string is not empty + s.Equal("UPPERCASE", Of("uppercase").WhenNotEmpty( + func(s *String) *String { + return s.Upper() + }, + ).String()) + + // Test when the string is empty + s.Equal("", Of("").WhenNotEmpty( + func(s *String) *String { + return s.Append("not empty") + }, + func(s *String) *String { + return s.Upper() + }, + ).String()) +} + +func (s *StringTestSuite) TestWhenStartsWith() { + // Test when the string starts with a specific prefix + s.Equal("Tony Stark", Of("tony stark").WhenStartsWith([]string{"ton"}, + func(s *String) *String { + return s.Title() + }, + func(s *String) *String { + return s.Studly() + }, + ).String()) + + // Test when the string starts with any of the specified prefixes + s.Equal("Tony Stark", Of("tony stark").WhenStartsWith([]string{"ton", "not"}, + func(s *String) *String { + return s.Title() + }, + func(s *String) *String { + return s.Studly() + }, + ).String()) + + // Test when the string does not start with the specified prefix + s.Equal("tony stark", Of("tony stark").WhenStartsWith([]string{"xxx"}, + func(s *String) *String { + return s.Title() + }, + ).String()) + + // Test when the string starts with one of the specified prefixes and not the other + s.Equal("Tony Stark", Of("tony stark").WhenStartsWith([]string{"tony", "xxx"}, + func(s *String) *String { + return s.Title() + }, + func(s *String) *String { + return s.Studly() + }, + ).String()) +} + +func (s *StringTestSuite) TestWhenEndsWith() { + // Test when the string ends with a specific suffix + s.Equal("Tony Stark", Of("tony stark").WhenEndsWith([]string{"ark"}, + func(s *String) *String { + return s.Title() + }, + func(s *String) *String { + return s.Studly() + }, + ).String()) + + // Test when the string ends with any of the specified suffixes + s.Equal("Tony Stark", Of("tony stark").WhenEndsWith([]string{"kra", "ark"}, + func(s *String) *String { + return s.Title() + }, + func(s *String) *String { + return s.Studly() + }, + ).String()) + + // Test when the string does not end with the specified suffix + s.Equal("tony stark", Of("tony stark").WhenEndsWith([]string{"xxx"}, + func(s *String) *String { + return s.Title() + }, + ).String()) + + // Test when the string ends with one of the specified suffixes and not the other + s.Equal("TonyStark", Of("tony stark").WhenEndsWith([]string{"tony", "xxx"}, + func(s *String) *String { + return s.Title() + }, + func(s *String) *String { + return s.Studly() + }, + ).String()) +} + +func (s *StringTestSuite) TestWhenExactly() { + // Test when the string exactly matches the expected value + s.Equal("Nailed it...!", Of("Tony Stark").WhenExactly("Tony Stark", + func(s *String) *String { + return Of("Nailed it...!") + }, + func(s *String) *String { + return Of("Swing and a miss...!") + }, + ).String()) + + // Test when the string does not exactly match the expected value + s.Equal("Swing and a miss...!", Of("Tony Stark").WhenExactly("Iron Man", + func(s *String) *String { + return Of("Nailed it...!") + }, + func(s *String) *String { + return Of("Swing and a miss...!") + }, + ).String()) + + // Test when the string exactly matches the expected value with no "else" callback + s.Equal("Tony Stark", Of("Tony Stark").WhenExactly("Iron Man", + func(s *String) *String { + return Of("Nailed it...!") + }, + ).String()) +} + +func (s *StringTestSuite) TestWhenNotExactly() { + // Test when the string does not exactly match the expected value with an "else" callback + s.Equal("Iron Man", Of("Tony").WhenNotExactly("Tony Stark", + func(s *String) *String { + return Of("Iron Man") + }, + ).String()) + + // Test when the string does not exactly match the expected value with both "if" and "else" callbacks + s.Equal("Swing and a miss...!", Of("Tony Stark").WhenNotExactly("Tony Stark", + func(s *String) *String { + return Of("Iron Man") + }, + func(s *String) *String { + return Of("Swing and a miss...!") + }, + ).String()) +} + +func (s *StringTestSuite) TestWhenIs() { + // Test when the string exactly matches the expected value with an "if" callback + s.Equal("Winner: /", Of("/").WhenIs("/", + func(s *String) *String { + return s.Prepend("Winner: ") + }, + func(s *String) *String { + return Of("Try again") + }, + ).String()) + + // Test when the string does not exactly match the expected value with an "if" callback + s.Equal("/", Of("/").WhenIs(" /", + func(s *String) *String { + return s.Prepend("Winner: ") + }, + ).String()) + + // Test when the string does not exactly match the expected value with both "if" and "else" callbacks + s.Equal("Try again", Of("/").WhenIs(" /", + func(s *String) *String { + return s.Prepend("Winner: ") + }, + func(s *String) *String { + return Of("Try again") + }, + ).String()) + + // Test when the string matches a pattern using wildcard and "if" callback + s.Equal("Winner: foo/bar/baz", Of("foo/bar/baz").WhenIs("foo/*", + func(s *String) *String { + return s.Prepend("Winner: ") + }, + ).String()) +} + +func (s *StringTestSuite) TestWhenIsUlid() { + // Test when the string is a valid ULID with an "if" callback + s.Equal("Ulid: 01GJSNW9MAF792C0XYY8RX6QFT", Of("01GJSNW9MAF792C0XYY8RX6QFT").WhenIsUlid( + func(s *String) *String { + return s.Prepend("Ulid: ") + }, + func(s *String) *String { + return s.Prepend("Not Ulid: ") + }, + ).String()) + + // Test when the string is not a valid ULID with an "if" callback + s.Equal("2cdc7039-65a6-4ac7-8e5d-d554a98", Of("2cdc7039-65a6-4ac7-8e5d-d554a98").WhenIsUlid( + func(s *String) *String { + return s.Prepend("Ulid: ") + }, + ).String()) + + // Test when the string is not a valid ULID with both "if" and "else" callbacks + s.Equal("Not Ulid: ss-01GJSNW9MAF792C0XYY8RX6QFT", Of("ss-01GJSNW9MAF792C0XYY8RX6QFT").WhenIsUlid( + func(s *String) *String { + return s.Prepend("Ulid: ") + }, + func(s *String) *String { + return s.Prepend("Not Ulid: ") + }, + ).String()) +} + +func (s *StringTestSuite) TestWhenIsUuid() { + // Test when the string is a valid UUID with an "if" callback + s.Equal("Uuid: 2cdc7039-65a6-4ac7-8e5d-d554a98e7b15", Of("2cdc7039-65a6-4ac7-8e5d-d554a98e7b15").WhenIsUuid( + func(s *String) *String { + return s.Prepend("Uuid: ") + }, + func(s *String) *String { + return s.Prepend("Not Uuid: ") + }, + ).String()) + + s.Equal("2cdc7039-65a6-4ac7-8e5d-d554a98", Of("2cdc7039-65a6-4ac7-8e5d-d554a98").WhenIsUuid( + func(s *String) *String { + return s.Prepend("Uuid: ") + }, + ).String()) + + s.Equal("Not Uuid: 2cdc7039-65a6-4ac7-8e5d-d554a98", Of("2cdc7039-65a6-4ac7-8e5d-d554a98").WhenIsUuid( + func(s *String) *String { + return s.Prepend("Uuid: ") + }, + func(s *String) *String { + return s.Prepend("Not Uuid: ") + }, + ).String()) +} + +func (s *StringTestSuite) TestWhenTest() { + // Test when the regular expression matches with an "if" callback + s.Equal("Winner: foo bar", Of("foo bar").WhenTest(`bar*`, + func(s *String) *String { + return s.Prepend("Winner: ") + }, + func(s *String) *String { + return Of("Try again") + }, + ).String()) + + // Test when the regular expression does not match with an "if" callback + s.Equal("Try again", Of("foo bar").WhenTest(`/link/`, + func(s *String) *String { + return s.Prepend("Winner: ") + }, + func(s *String) *String { + return Of("Try again") + }, + ).String()) + + // Test when the regular expression does not match with both "if" and "else" callbacks + s.Equal("foo bar", Of("foo bar").WhenTest(`/link/`, + func(s *String) *String { + return s.Prepend("Winner: ") + }, + ).String()) +} + +func (s *StringTestSuite) TestWordCount() { + s.Equal(2, Of("Hello, world!").WordCount()) + s.Equal(10, Of("Hi, this is my first contribution to the Goravel framework.").WordCount()) +} + +func (s *StringTestSuite) TestWords() { + s.Equal("Perfectly balanced, as >>>", Of("Perfectly balanced, as all things should be.").Words(3, " >>>").String()) + s.Equal("Perfectly balanced, as all things should be.", Of("Perfectly balanced, as all things should be.").Words(100).String()) +} + +func TestFieldsFunc(t *testing.T) { + tests := []struct { + input string + shouldPreserve []func(rune) bool + expected []string + }{ + // Test case 1: Basic word splitting with space separator. + { + input: "Hello World", + expected: []string{"Hello", "World"}, + }, + // Test case 2: Splitting with space and preserving hyphen. + { + input: "Hello-World", + shouldPreserve: []func(rune) bool{func(r rune) bool { return r == '-' }}, + expected: []string{"Hello", "-World"}, + }, + // Test case 3: Splitting with space and preserving multiple characters. + { + input: "Hello-World,This,Is,a,Test", + shouldPreserve: []func(rune) bool{ + func(r rune) bool { return r == '-' }, + func(r rune) bool { return r == ',' }, + }, + expected: []string{"Hello", "-World", ",This", ",Is", ",a", ",Test"}, + }, + // Test case 4: No splitting when no separator is found. + { + input: "HelloWorld", + expected: []string{"HelloWorld"}, + }, + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + result := fieldsFunc(test.input, func(r rune) bool { return r == ' ' }, test.shouldPreserve...) + assert.Equal(t, test.expected, result) + }) + } +} + +func TestSubstr(t *testing.T) { + assert.Equal(t, "world", Substr("Hello, world!", 7, 5)) + assert.Equal(t, "", Substr("Golang", 10)) + assert.Equal(t, "tine", Substr("Goroutines", -5, 4)) + assert.Equal(t, "ic", Substr("Unicode", 2, -3)) + assert.Equal(t, "esting", Substr("Testing", 1, 10)) + assert.Equal(t, "", Substr("", 0, 5)) + assert.Equal(t, "世界!", Substr("你好,世界!", 3, 3)) +} + +func TestMaximum(t *testing.T) { + assert.Equal(t, 10, maximum(5, 10)) + assert.Equal(t, 3.14, maximum(3.14, 2.71)) + assert.Equal(t, "banana", maximum("apple", "banana")) + assert.Equal(t, -5, maximum(-5, -10)) + assert.Equal(t, 42, maximum(42, 42)) +} + func TestRandom(t *testing.T) { assert.Len(t, Random(10), 10) assert.Empty(t, Random(0)) + assert.Panics(t, func() { + Random(-1) + }) } func TestCase2Camel(t *testing.T) {