sq/libsq/core/stringz/stringz.go

402 lines
8.7 KiB
Go

// Package stringz contains string functions similar in spirit
// to the stdlib strings package.
package stringz
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"math/rand"
"strconv"
"strings"
"time"
"unicode"
"github.com/google/uuid"
"github.com/neilotoole/sq/libsq/core/errz"
)
// Reverse reverses the input string.
func Reverse(input string) string {
n := 0
runes := make([]rune, len(input))
for _, r := range input {
runes[n] = r
n++
}
runes = runes[0:n]
// Reverse
for i := 0; i < n/2; i++ {
runes[i], runes[n-1-i] = runes[n-1-i], runes[i]
}
// Convert back to UTF-8.
return string(runes)
}
// GenerateAlphaColName returns an Excel-style column name
// for index n, starting with A, B, C... and continuing
// to AA, AB, AC, etc...
func GenerateAlphaColName(n int, lower bool) string {
start := 'A'
if lower {
start = 'a'
}
return genAlphaCol(n, start, 26)
}
func genAlphaCol(n int, start rune, lenAlpha int) string {
buf := &bytes.Buffer{}
for ; n >= 0; n = (n / lenAlpha) - 1 {
buf.WriteRune(rune(n%lenAlpha) + start)
}
return Reverse(buf.String())
}
// ParseBool is an expansion of strconv.ParseBool that also
// accepts variants of "yes" and "no" (which are bool
// representations returned by some data sources).
func ParseBool(s string) (bool, error) {
switch s {
default:
b, err := strconv.ParseBool(s)
if err != nil {
return b, errz.Err(err)
}
return b, nil
case "1", "yes", "Yes", "YES", "y", "Y":
return true, nil
case "0", "no", "No", "NO", "n", "N":
return false, nil
}
}
// InSlice returns true if the needle is present in the haystack.
func InSlice(haystack []string, needle string) bool {
return SliceIndex(haystack, needle) != -1
}
// SliceIndex returns the index of needle in haystack, or -1.
func SliceIndex(haystack []string, needle string) int {
for i, item := range haystack {
if item == needle {
return i
}
}
return -1
}
// FormatFloat formats f. This method exists to provide a standard
// float formatting across the codebase.
func FormatFloat(f float64) string {
return strconv.FormatFloat(f, 'f', -1, 64)
}
// ByteSized returns a human-readable byte size, e.g. "2.1 MB", "3.0 TB", etc.
// TODO: replace this usage with "github.com/c2h5oh/datasize"
func ByteSized(size int64, precision int, sep string) string {
f := float64(size)
tpl := "%." + strconv.Itoa(precision) + "f" + sep
switch {
case f >= yb:
return fmt.Sprintf(tpl+"YB", f/yb)
case f >= zb:
return fmt.Sprintf(tpl+"ZB", f/zb)
case f >= eb:
return fmt.Sprintf(tpl+"EB", f/eb)
case f >= pb:
return fmt.Sprintf(tpl+"PB", f/pb)
case f >= tb:
return fmt.Sprintf(tpl+"TB", f/tb)
case f >= gb:
return fmt.Sprintf(tpl+"GB", f/gb)
case f >= mb:
return fmt.Sprintf(tpl+"MB", f/mb)
case f >= kb:
return fmt.Sprintf(tpl+"KB", f/kb)
}
return fmt.Sprintf(tpl+"B", f)
}
const (
_ = iota // ignore first value by assigning to blank identifier
kb float64 = 1 << (10 * iota)
mb
gb
tb
pb
eb
zb
yb
)
func SprintJSON(value any) string {
j, err := json.MarshalIndent(value, "", " ")
if err != nil {
panic(err)
}
return string(j)
}
// UUID returns a new UUID string.
func UUID() string {
return uuid.New().String()
}
// Uniq32 returns a UUID-like string that only contains
// alphanumeric chars. The result has length 32.
// The first element is guaranteed to be a letter.
func Uniq32() string {
return UniqN(32)
}
// Uniq8 returns a UUID-like string that only contains
// alphanumeric chars. The result has length 8.
// The first element is guaranteed to be a letter.
func Uniq8() string {
// I'm sure there's a more efficient way of doing this, but
// this is fine for now.
return UniqN(8)
}
// UniqSuffix returns s with a unique suffix.
func UniqSuffix(s string) string {
return s + "_" + Uniq8()
}
// UniqPrefix returns s with a unique prefix.
func UniqPrefix(s string) string {
return Uniq8() + "_" + s
}
const (
// charsetAlphanumericLower is a set of characters to generate from. Note
// that ambiguous chars such as "i" or "j" are excluded.
charsetAlphanumericLower = "abcdefghkrstuvwxyz2345689"
// charsetAlphaLower is similar to charsetAlphanumericLower, but
// without numbers.
charsetAlphaLower = "abcdefghkrstuvwxyz"
)
func stringWithCharset(length int, charset string) string {
if charset == "" {
panic("charset has zero length")
}
if length <= 0 {
return ""
}
b := make([]byte, length)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}
// UniqN returns a uniq string of length n. The first element is
// guaranteed to be a letter.
func UniqN(length int) string {
switch {
case length <= 0:
return ""
case length == 1:
return stringWithCharset(1, charsetAlphaLower)
default:
return stringWithCharset(1, charsetAlphaLower) + stringWithCharset(length-1, charsetAlphanumericLower)
}
}
// Plu handles the most common (English language) case of
// pluralization. With arg s being "row(s) col(s)", Plu
// returns "row col" if arg i is 1, otherwise returns "rows cols".
func Plu(s string, i int) string {
if i == 1 {
return strings.Replace(s, "(s)", "", -1)
}
return strings.Replace(s, "(s)", "s", -1)
}
// RepeatJoin returns a string consisting of count copies
// of s separated by sep. For example:
//
// stringz.RepeatJoin("?", 3, ", ") == "?, ?, ?"
func RepeatJoin(s string, count int, sep string) string {
if s == "" || count == 0 {
return ""
}
if count == 1 {
return s
}
var b strings.Builder
b.Grow(len(s)*count + len(sep)*(count-1))
for i := 0; i < count; i++ {
b.WriteString(s)
if i < count-1 {
b.WriteString(sep)
}
}
return b.String()
}
// Surround returns s prefixed and suffixed with w.
func Surround(s, w string) string {
sb := strings.Builder{}
sb.Grow(len(s) + len(w)*2)
sb.WriteString(w)
sb.WriteString(s)
sb.WriteString(w)
return sb.String()
}
// SurroundSlice returns a new slice with each element
// of a prefixed and suffixed with w, unless a is nil,
// in which case nil is returned.
func SurroundSlice(a []string, w string) []string {
if a == nil {
return nil
}
if len(a) == 0 {
return []string{}
}
ret := make([]string, len(a))
sb := strings.Builder{}
for i := 0; i < len(a); i++ {
sb.Grow(len(a[i]) + len(w)*2)
sb.WriteString(w)
sb.WriteString(a[i])
sb.WriteString(w)
ret[i] = sb.String()
sb.Reset()
}
return ret
}
// PrefixSlice returns a new slice with each element
// of a prefixed with w, unless a is nil, in which
// case nil is returned.
func PrefixSlice(a []string, w string) []string {
if a == nil {
return nil
}
if len(a) == 0 {
return []string{}
}
ret := make([]string, len(a))
sb := strings.Builder{}
for i := 0; i < len(a); i++ {
sb.Grow(len(a[i]) + len(w))
sb.WriteString(w)
sb.WriteString(a[i])
ret[i] = sb.String()
sb.Reset()
}
return ret
}
const (
// DateFormat is the layout for dates (without a time component), such as 2006-01-02.
DateFormat = "2006-01-02"
// TimeFormat is the layout for 24-hour time (without a date component), such as 15:04:05.
TimeFormat = "15:04:05"
// DatetimeFormat is the layout for a date/time timestamp.
DatetimeFormat = time.RFC3339Nano
)
// UniqTableName returns a new lower-case table name based on
// tbl, with a unique suffix, and a maximum length of 63. This
// value of 63 is chosen because it's less than the maximum table name
// length for Postgres, SQL Server, SQLite and MySQL.
func UniqTableName(tbl string) string {
const maxLength = 63
tbl = strings.TrimSpace(tbl)
tbl = strings.ToLower(tbl)
if tbl == "" {
tbl = "tbl"
}
suffix := "__" + Uniq8()
if len(tbl) > maxLength-len(suffix) {
tbl = tbl[0 : maxLength-len(suffix)]
}
tbl += suffix
// paranoid sanitization
tbl = strings.Replace(tbl, "@", "_", -1)
tbl = strings.Replace(tbl, "/", "_", -1)
return tbl
}
// SanitizeAlphaNumeric replaces any non-alphanumeric
// runes of s with r (which is typically underscore).
//
// a#2%3.4_ --> a_2_3_4_
func SanitizeAlphaNumeric(s string, r rune) string {
runes := []rune(s)
for i, v := range runes {
switch {
case v == r, unicode.IsLetter(v), unicode.IsNumber(v):
default:
runes[i] = r
}
}
return string(runes)
}
// LineCount returns the number of lines in r. If skipEmpty is
// true, empty lines are skipped (a whitespace-only line is not
// considered empty). If r is nil or any error occurs, -1 is returned.
func LineCount(r io.Reader, skipEmpty bool) int {
if r == nil {
return -1
}
sc := bufio.NewScanner(r)
var i int
if skipEmpty {
for sc.Scan() {
if len(sc.Bytes()) > 0 {
i++
}
}
if sc.Err() != nil {
return -1
}
return i
}
for i = 0; sc.Scan(); i++ {
}
return i
}
// TrimLen returns s but with a maximum length of maxLen.
func TrimLen(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen]
}