package string_utils import ( "bytes" "encoding/json" "fmt" "github.com/samber/lo" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/number_utils" "regexp" "strconv" "strings" "unicode" "github.com/thoas/go-funk" "golang.org/x/text/runes" "golang.org/x/text/transform" "golang.org/x/text/unicode/norm" ) const ( snakeCasePattern = `[a-z]([a-z0-9_]*[a-z0-9])*` regexIndexMatchStart = 0 regexIndexMatchEnd = 1 regexIndexSubmatchStart = 2 regexIndexSubmatchEnd = 3 ) var WhitespaceChars = []string{ // Standard whitespace characters "\u0009", // Character tabulation "\u000A", // Line feed "\u000B", // Line tabulation "\u000C", // Form feed "\u000D", // Carriage return "\u0020", // Space "\u0085", // Next line "\u00A0", // No-break space "\u1680", // Ogham space mark "\u2000", // En quad "\u2001", // Em quad "\u2002", // En space "\u2003", // Em space "\u2004", // Three-per-em space "\u2005", // Four-per-em space "\u2006", // Six-per-em space "\u2007", // Figure space "\u2008", // Punctuation space "\u2009", // Thin space "\u200A", // Hair space "\u2028", // Line separator "\u2029", // Paragraph separator "\u202F", // Narrow no-break space "\u205F", // Medium mathematical space "\u3000", // Ideographic space } var NonOfficialWhitespaceChars = []string{ // Characters with property White_Space=no "\u180E", // Mongolian vowel separator "\u200B", // Zero width space "\u200C", // Zero width non-joiner "\u200D", // Zero width joiner "\u200E", // Left-to-right mark "\u2060", // Word joiner "\u202C", // Pop directional formatting "\uFEFF", // Zero width no-break space "\u00AD", // Soft hyphen } var snakeCaseRegex = regexp.MustCompile("^" + snakeCasePattern + "$") func IsSnakeCase(name string) bool { return snakeCaseRegex.MatchString(name) } func SnakeToKebabString(s string) string { s = strings.TrimSpace(s) re := regexp.MustCompile("(_)") s = re.ReplaceAllString(s, "-") return s } // ReplaceNonSpacingMarks removes diacritics e.g. êžů becomes ezu func ReplaceNonSpacingMarks(str string) string { t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) // Mn: non-spacing marks result, _, _ := transform.String(t, str) return result } func RemoveAllWhiteSpaces(s string) string { cleanedString := strings.ReplaceAll(strings.ReplaceAll(s, " ", ""), "\t", "") for _, whitespaceChar := range WhitespaceChars { cleanedString = strings.ReplaceAll(cleanedString, whitespaceChar, "") } return cleanedString } func ReplaceCaseInsensitive(string, toReplace, replaceWith string) string { regex := regexp.MustCompile("(?i)" + strings.ToLower(toReplace)) return regex.ReplaceAllString(string, replaceWith) } // ReplaceAllRegexStringSubmatch finds the submatches for a regular expression that has a single capturing group and // replaces all the submatches (i.e. the part that matches the capturing group) with replaceWith. // E.g. the regular expression re = a(x*)b captures any number of x's that are between an a and b. // ReplaceAllRegexStringSubmatch(re, "-axxb-ab-axb-x-ax-xb-ba-", "?") will result in "-a?b-a?b-a?b-x-ax-xb-ba-" func ReplaceAllRegexStringSubmatch(re *regexp.Regexp, s string, replaceWith string) string { result := "" lastIndex := 0 for _, v := range re.FindAllSubmatchIndex([]byte(s), -1) { if lo.Contains(v, -1) { continue } if len(v) == regexIndexSubmatchEnd+1 { // One submatch - replace the submatch with replaceWith result += s[lastIndex:v[regexIndexSubmatchStart]] + replaceWith + s[v[regexIndexSubmatchEnd]:v[regexIndexMatchEnd]] lastIndex = v[regexIndexMatchEnd] } else { // A normal match with no submatch - don't replace anything (this should not really happen) result += s[lastIndex:v[regexIndexMatchEnd]] } lastIndex = v[regexIndexMatchEnd] } return result + s[lastIndex:] } // TrimQuotes - trims quotes from a string (ie: "foo" will return foo) func TrimQuotes(stringToTrim string) string { if len(stringToTrim) >= 2 { if stringToTrim[0] == '"' && stringToTrim[len(stringToTrim)-1] == '"' { return stringToTrim[1 : len(stringToTrim)-1] } } return stringToTrim } // IsNumericString returns true if the string can be converted to a float without error func IsNumericString(s string) bool { _, err := strconv.ParseFloat(s, 64) return err == nil } func IsAlphaNumeric(str string) bool { regex := regexp.MustCompile("^[a-zA-Z0-9]*$") return regex.MatchString(str) } func IsAlphaNumericOrDash(str string) bool { regex := regexp.MustCompile("^[a-zA-Z0-9-]*$") return regex.MatchString(str) } func Equal(a string, b string) bool { return strings.TrimSpace(strings.ToLower(a)) == strings.TrimSpace(strings.ToLower(b)) } // TrimP trims specified strings, replacing empty string with nil func TrimP(sp *string) *string { if sp == nil { return nil } s := strings.TrimSpace(*sp) if s == "" { return nil } return &s } // ConcatP concatenates all specified non-empty strings with ", " separators func ConcatP(args ...*string) string { s := "" for _, arg := range args { if arg != nil && *arg != "" { if s != "" { s += ", " } s += *arg } } return s } // Concat concatenates all specified non-empty strings with ", " separators func Concat(args []string, separator string) string { s := "" for _, arg := range args { if arg != "" { if s != "" { s += separator } s += arg } } return s } func ToJSONString(object interface{}) (string, error) { jsonBytes, err := json.Marshal(&object) if err != nil { return "", err } return string(jsonBytes), nil } func Int64ToString(number int64) string { return strconv.FormatInt(number, 10) } func IntToString(number int) string { return strconv.Itoa(number) } func Int64SliceToString(numbers []int64) string { numString := fmt.Sprint(numbers) numString = strings.Join(strings.Split(numString, " "), ",") return numString } func Int64SliceToStringSlice(numbers []int64) []string { var numStringSlice []string for _, number := range numbers { numStringSlice = append(numStringSlice, fmt.Sprint(number)) } return numStringSlice } func Float64ToString(number float64, precision int) string { return strconv.FormatFloat(number, 'f', precision, 64) } func Float64ToStringWithPrec(number float64, prec int) string { return strconv.FormatFloat(number, 'f', prec, 64) } func ValidateStringAsInt64(stringValue string) error { _, err := number_utils.StringToInt64(stringValue) return err } func IsEmpty(sp *string) bool { if sp == nil { return true } return len(*sp) == 0 } // StringTrimQuotes - trims quotes from a string (ie: "foo" will return foo) func StringTrimQuotes(stringToTrim string) string { if len(stringToTrim) >= 2 { if stringToTrim[0] == '"' && stringToTrim[len(stringToTrim)-1] == '"' { return stringToTrim[1 : len(stringToTrim)-1] } } return stringToTrim } func KeyToHumanReadable(s string) string { s = strings.TrimSpace(s) re := regexp.MustCompile("(_|-)") s = re.ReplaceAllString(s, " ") return SentenceCase(string(s)) } func HumanReadableToKey(s string, separator string) string { re := regexp.MustCompile(" +") s = re.ReplaceAllString(s, separator) s = strings.ToLower(s) return s } func SentenceCase(str string) string { if len(str) > 0 { str = strings.ToLower(str) r := []rune(str) return string(append([]rune{unicode.ToUpper(r[0])}, r[1:]...)) } return "" } func Capitalize(s string) string { if len(s) == 0 { return s } return string(unicode.ToUpper(rune(s[0]))) + s[1:] } // SplitString separates a string on any character in the list of sep func SplitString(str string, sep []rune) []string { splitStrings := strings.FieldsFunc(str, func(c rune) bool { return funk.Contains(sep, c) }) return splitStrings } func LimitStringToMaxLength(str string, maxLen int) string { if len(str) > maxLen { str = str[0:maxLen] } return str } func PascalCaseToSentence(pascal string) string { var parts []string start := 0 for end, r := range pascal { if end != 0 && unicode.IsUpper(r) { parts = append(parts, pascal[start:end]) start = end } } if start != len(pascal) { parts = append(parts, pascal[start:]) } sentence := strings.Join(parts, " ") return sentence } func PrettyJSON(jsonString string) (validJson bool, prettyString string) { var prettyJSON bytes.Buffer err := json.Indent(&prettyJSON, []byte(jsonString), "", " ") if err != nil { validJson = false prettyString = jsonString } else { validJson = true prettyString = prettyJSON.String() } return } func TrimSpaceForPointer(s *string) *string { if s != nil { trimmedString := strings.TrimSpace(*s) return &trimmedString } return s } // TrimAndToLower trims the whitespace from a string and converts it to lowercase func TrimAndToLower(s string) string { return strings.ToLower(strings.TrimSpace(s)) } func StringContainsNumbers(s string) bool { // Define a regular expression to match numbers regex := regexp.MustCompile("[0-9]") // Use FindString to check if the string contains any numbers return regex.MatchString(s) } func StringContainsOnlyNumbers(s string) bool { for _, char := range s { if !unicode.IsDigit(char) { return false } } return true }