644 lines
14 KiB
Go
644 lines
14 KiB
Go
// General purpose "utilities" that act as my own "standard library"
|
|
package basic
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"os"
|
|
"reflect"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// Equivalent to Atoi, but returns int32 rather than (int, error)
|
|
func Atoi32(s string) int32 {
|
|
i, _ := strconv.ParseInt(s, 10, 32)
|
|
return int32(i)
|
|
}
|
|
|
|
// Equivalent to Atoi, but returns int64 rather than (int, error)
|
|
func Atoi64(s string) int64 {
|
|
i, _ := strconv.ParseInt(s, 10, 64)
|
|
return i
|
|
}
|
|
|
|
func GetPathParts(path string) []string {
|
|
trimmed := strings.TrimPrefix(path, "/")
|
|
return strings.Split(trimmed, "/")
|
|
}
|
|
|
|
// Takes a tree pointer and a slice of path segments to insert.
|
|
// It looks for an existing child with the current segment name; if none is found,
|
|
// it creates a new node on the tree. Then it recurses on the remaining segments.
|
|
//
|
|
// Ex: Generate tree nodes from url segments
|
|
// `/app/examples/webpage` -> {"app", "examples", "webpage"}
|
|
// `/app/examples/hello-world` -> {"app", "examples", "hello-world"
|
|
// `/auth/login` -> {"auth", "login"}
|
|
//
|
|
// =>
|
|
//
|
|
// root {
|
|
// app {
|
|
// examples {
|
|
// webpage
|
|
// hello-world
|
|
// }
|
|
// }
|
|
//
|
|
// auth {
|
|
// login
|
|
// }
|
|
// }
|
|
//
|
|
// This function is used in the code generation process to generate `pageinfo` structs
|
|
// from all known application page URLs.
|
|
func AddStringPartsToTree(tree *Tree, parts []string) {
|
|
if len(parts) == 0 {
|
|
return
|
|
}
|
|
|
|
for i := range parts {
|
|
if parts[i] == "" {
|
|
parts[i] = "index"
|
|
}
|
|
}
|
|
|
|
if tree.Children == nil {
|
|
tree.Children = new([]Tree)
|
|
}
|
|
|
|
// Search for an existing child with the current part's name.
|
|
var child *Tree
|
|
for i := range *tree.Children {
|
|
if (*tree.Children)[i].Name == parts[0] {
|
|
child = &((*tree.Children)[i])
|
|
break
|
|
}
|
|
}
|
|
|
|
// If no child is found, create a new one and append it.
|
|
if child == nil {
|
|
newNode := Tree{Name: parts[0]}
|
|
*tree.Children = append(*tree.Children, newNode)
|
|
child = &((*tree.Children)[len(*tree.Children)-1])
|
|
}
|
|
|
|
AddStringPartsToTree(child, parts[1:])
|
|
}
|
|
|
|
type Tree struct {
|
|
Name string
|
|
Children *[]Tree
|
|
}
|
|
|
|
func CapitalizeFirstLetter(s string) string {
|
|
if s == "" {
|
|
return s
|
|
}
|
|
// Convert the first rune to uppercase
|
|
first := []rune(s)[0]
|
|
return string(unicode.ToUpper(first)) + s[1:]
|
|
}
|
|
|
|
// Known compound surnames where a simple prefix rule would cause false positives
|
|
// (e.g. "De" matches Dean/Dennis, "La" matches Laura/Lance). Add entries as needed.
|
|
var compoundSurnames = map[string]string{
|
|
// De-
|
|
"deangelo": "DeAngelo", "decarlo": "DeCarlo", "dejesus": "DeJesus",
|
|
"deleon": "DeLeon", "deluca": "DeLuca", "demarco": "DeMarco",
|
|
"depaul": "DePaul", "derosa": "DeRosa", "desantis": "DeSantis",
|
|
"devries": "DeVries", "devris": "DeVris", "dewitt": "DeWitt",
|
|
"deyoung": "DeYoung",
|
|
// Di-
|
|
"dicaprio": "DiCaprio", "dicarlo": "DiCarlo", "dimaggio": "DiMaggio",
|
|
"dinapoli": "DiNapoli", "dipietro": "DiPietro",
|
|
// La-
|
|
"lafleur": "LaFleur", "lafrance": "LaFrance", "lamontagne": "LaMontagne",
|
|
"laporte": "LaPorte", "larocca": "LaRocca", "larue": "LaRue",
|
|
"lasalle": "LaSalle",
|
|
// Le-
|
|
"leblanc": "LeBlanc", "lebron": "LeBron", "legrand": "LeGrand",
|
|
"lemay": "LeMay",
|
|
// Lo-
|
|
"lopresti": "LoPresti",
|
|
// Du-
|
|
"dubois": "DuBois", "dupont": "DuPont", "dupree": "DuPree",
|
|
}
|
|
|
|
// NormalizeName intelligently capitalizes a name field (first or last name).
|
|
// Handles edge cases like O'Brian, McDonald, MacArthur, hyphenated names,
|
|
// compound surnames (DeSantis, DiCaprio, LeBlanc), and suffixes (Jr, III).
|
|
func NormalizeName(name string) string {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return name
|
|
}
|
|
|
|
// If the name has mixed capitalization, assume the user typed it intentionally.
|
|
hasUpper, hasLower := false, false
|
|
for _, r := range name {
|
|
if unicode.IsUpper(r) {
|
|
hasUpper = true
|
|
} else if unicode.IsLower(r) {
|
|
hasLower = true
|
|
}
|
|
if hasUpper && hasLower {
|
|
return name
|
|
}
|
|
}
|
|
|
|
name = strings.ToLower(name)
|
|
|
|
hyphenParts := strings.Split(name, "-")
|
|
for i, hpart := range hyphenParts {
|
|
words := strings.Fields(hpart)
|
|
for j, word := range words {
|
|
if mapped, ok := compoundSurnames[word]; ok {
|
|
words[j] = mapped
|
|
continue
|
|
}
|
|
|
|
runes := []rune(word)
|
|
|
|
switch {
|
|
case word == "ii" || word == "iii" || word == "iv" || word == "vi" || word == "vii" || word == "viii":
|
|
words[j] = strings.ToUpper(word)
|
|
case word == "jr" || word == "sr":
|
|
words[j] = CapitalizeFirstLetter(word) + "."
|
|
case strings.HasPrefix(word, "mc") && len(runes) >= 3:
|
|
words[j] = "Mc" + string(unicode.ToUpper(runes[2])) + string(runes[3:])
|
|
case strings.HasPrefix(word, "mac") && len(runes) >= 5:
|
|
words[j] = "Mac" + string(unicode.ToUpper(runes[3])) + string(runes[4:])
|
|
case strings.HasPrefix(word, "o'") && len(runes) >= 3:
|
|
words[j] = "O'" + string(unicode.ToUpper(runes[2])) + string(runes[3:])
|
|
default:
|
|
words[j] = string(unicode.ToUpper(runes[0])) + string(runes[1:])
|
|
}
|
|
}
|
|
hyphenParts[i] = strings.Join(words, " ")
|
|
}
|
|
return strings.Join(hyphenParts, "-")
|
|
}
|
|
|
|
var profanityList = map[string]bool{
|
|
// Common profanity — whole-word matches only, so substrings in real names
|
|
// (e.g. "Massimo", "Dickens", "Cockburn") are not flagged.
|
|
"ass": true, "arse": true, "asshole": true,
|
|
"bastard": true, "bitch": true, "bollocks": true,
|
|
"cock": true, "crap": true, "cunt": true,
|
|
"damn": true, "dildo": true,
|
|
"fag": true, "fuck": true, "fucker": true,
|
|
"goddamn": true,
|
|
"jackass": true,
|
|
"motherfucker": true,
|
|
"nigger": true, "nigga": true,
|
|
"piss": true, "prick": true, "pussy": true,
|
|
"shit": true, "slut": true,
|
|
"tit": true, "tits": true, "twat": true,
|
|
"wanker": true, "whore": true,
|
|
}
|
|
|
|
var leetReplacer = strings.NewReplacer(
|
|
"0", "o",
|
|
"1", "i",
|
|
"3", "e",
|
|
"4", "a",
|
|
"5", "s",
|
|
"7", "t",
|
|
"8", "b",
|
|
"@", "a",
|
|
"$", "s",
|
|
"!", "i",
|
|
)
|
|
|
|
// NameContainsProfanity checks whether any whole word in the name matches a known profane term.
|
|
// Words are split on spaces, hyphens, and apostrophes so that substrings within legitimate
|
|
// names (e.g. "Massimo", "Dickens", "Cockburn") are not flagged.
|
|
// Also normalizes leet speak substitutions (e.g. "b1tch", "a$$", "sh!t").
|
|
func NameContainsProfanity(name string) bool {
|
|
name = strings.ToLower(name)
|
|
parts := strings.FieldsFunc(name, func(r rune) bool {
|
|
return r == ' ' || r == '-' || r == '\''
|
|
})
|
|
for _, part := range parts {
|
|
if profanityList[part] {
|
|
return true
|
|
}
|
|
// Check leet speak variant
|
|
normalized := leetReplacer.Replace(part)
|
|
if normalized != part && profanityList[normalized] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func IntAbs(x int) int {
|
|
if x < 0 {
|
|
return -x
|
|
}
|
|
|
|
return x
|
|
}
|
|
|
|
func MakeURLParams(base string, params ...[2]string) string {
|
|
output := base
|
|
|
|
for i, v := range params {
|
|
if i == 0 {
|
|
output += "?" + v[0] + "=" + v[1]
|
|
} else {
|
|
output += "&" + v[0] + "=" + v[1]
|
|
}
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
func ToSnakeCase(s string) string {
|
|
s = strings.ReplaceAll(s, " ", "_")
|
|
|
|
return s
|
|
}
|
|
|
|
func SnakeCaseToTitleCase(s string) string {
|
|
parts := strings.Split(s, "_")
|
|
|
|
for i, part := range parts {
|
|
parts[i] = CapitalizeFirstLetter(part)
|
|
}
|
|
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
func NullableToString[T any](i *T) string {
|
|
if i == nil {
|
|
return ""
|
|
}
|
|
|
|
return ToString(*i)
|
|
}
|
|
|
|
func ToString(i any) string {
|
|
v := reflect.ValueOf(i)
|
|
|
|
output := ""
|
|
|
|
switch v.Kind() {
|
|
case reflect.String:
|
|
output = v.String()
|
|
case reflect.Int:
|
|
output = fmt.Sprintf("%d", v.Int())
|
|
case reflect.Float64:
|
|
output = fmt.Sprintf("%f", v.Float())
|
|
case reflect.Bool:
|
|
output = fmt.Sprintf("%t", v.Bool())
|
|
default:
|
|
output = fmt.Sprintf("%v", i)
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
func Int64ToStringWithCommas(i int64) string {
|
|
str := strconv.Itoa(int(i))
|
|
|
|
negative := false
|
|
if strings.HasPrefix(str, "-") {
|
|
negative = true
|
|
str = str[1:]
|
|
}
|
|
|
|
result := ""
|
|
for i, char := range str {
|
|
if i > 0 && (len(str)-i)%3 == 0 {
|
|
result += ","
|
|
}
|
|
result += string(char)
|
|
}
|
|
|
|
if negative {
|
|
result = "-" + result
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Strips non-numeric characters from a string, then converts digits in string to int
|
|
func StringToInt(input string) int {
|
|
i, _ := strconv.Atoi(SanitizeNum(input))
|
|
return i
|
|
}
|
|
|
|
// Strips non-numeric characters from a string, then converts digits in string to int32
|
|
func StringToInt32(input string) int32 {
|
|
return Atoi32(SanitizeNum(input))
|
|
}
|
|
|
|
// Strips non-numeric characters from a string, then converts digits in string to int64
|
|
func StringToInt64(input string) int64 {
|
|
return Atoi64(SanitizeNum(input))
|
|
}
|
|
|
|
// ABC
|
|
func SafeIndex[T any](index int, arr []T) T {
|
|
var temp T
|
|
|
|
if index > len(arr)-1 {
|
|
return temp
|
|
}
|
|
|
|
return arr[index]
|
|
}
|
|
|
|
func SafeDereference[T any](value *T, optionalDefault ...T) T {
|
|
if value != nil {
|
|
return *value
|
|
}
|
|
|
|
if len(optionalDefault) > 0 {
|
|
return optionalDefault[0]
|
|
}
|
|
|
|
return *new(T)
|
|
}
|
|
|
|
func MakePtr[T any](value T) *T {
|
|
output := new(T)
|
|
*output = value
|
|
return output
|
|
}
|
|
|
|
func Reverse[T comparable](s []T) {
|
|
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
|
s[i], s[j] = s[j], s[i]
|
|
}
|
|
}
|
|
|
|
// Given two arrays, make sure that elements in array 1 are present in array 2
|
|
// Return false if the requirements are not met
|
|
func ContainsAll[T comparable](arr1, arr2 []T) bool {
|
|
elements := make(map[T]bool)
|
|
|
|
for _, num := range arr1 {
|
|
elements[num] = true
|
|
}
|
|
|
|
for _, num := range arr2 {
|
|
if !elements[num] {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func IndexOf[T comparable](collection []T, el T) int {
|
|
for i, x := range collection {
|
|
if x == el {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func Remove[T comparable](arr []T, s T) []T {
|
|
return append(arr[:IndexOf(arr, s)], arr[IndexOf(arr, s)+1:]...)
|
|
}
|
|
|
|
func RemoveDuplicates[T comparable](sliceList []T) []T {
|
|
allKeys := make(map[T]bool)
|
|
list := []T{}
|
|
for _, item := range sliceList {
|
|
if _, value := allKeys[item]; !value {
|
|
allKeys[item] = true
|
|
list = append(list, item)
|
|
}
|
|
}
|
|
return list
|
|
}
|
|
|
|
// Return elements in slice 1 minus elements in slice2
|
|
func RemoveMany[T comparable](slice1, slice2 []T) []T {
|
|
removeMap := make(map[T]bool)
|
|
for _, item := range slice2 {
|
|
removeMap[item] = true
|
|
}
|
|
|
|
result := make([]T, 0)
|
|
|
|
for _, item := range slice1 {
|
|
if !removeMap[item] {
|
|
result = append(result, item)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func GetFirstNChars(s string, n int) string {
|
|
i := 0
|
|
for j := range s {
|
|
if i == n {
|
|
return s[:j]
|
|
}
|
|
i++
|
|
}
|
|
return s
|
|
}
|
|
|
|
func PrintStatus(b bool) {
|
|
var status string
|
|
|
|
if b {
|
|
status = "SUCCESS"
|
|
} else {
|
|
status = "FAILED"
|
|
}
|
|
|
|
fmt.Printf("... %s\n", status)
|
|
}
|
|
|
|
func DirExists(path string) (bool, error) {
|
|
_, err := os.Stat(path)
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
func MapToSortedArray(m map[string]int64) [][2]int64 {
|
|
pairs := make([][2]int64, 0, len(m))
|
|
|
|
for k, v := range m {
|
|
numKey, err := strconv.ParseInt(k, 10, 64)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
pairs = append(pairs, [2]int64{numKey, v})
|
|
}
|
|
|
|
sort.Slice(pairs, func(i, j int) bool {
|
|
return pairs[i][0] < pairs[j][0]
|
|
})
|
|
|
|
return pairs
|
|
}
|
|
|
|
func RandomSortInt32(n int) []int32 {
|
|
result := make([]int32, n)
|
|
for i := 0; i < n; i++ {
|
|
result[i] = int32(i + 1)
|
|
}
|
|
|
|
// Shuffle the list using Fisher-Yates algorithm
|
|
for i := n - 1; i > 0; i-- {
|
|
j := rand.Intn(i + 1)
|
|
result[i], result[j] = result[j], result[i]
|
|
}
|
|
return result
|
|
}
|
|
|
|
func SlicesEqual[T comparable](a []T, b []T) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
|
|
for i := range a {
|
|
if b[i] != a[i] {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
type StructDiff struct {
|
|
FieldName string `json:"field_name"`
|
|
OldValue any `json:"old_value"`
|
|
NewValue any `json:"new_value"`
|
|
}
|
|
|
|
type StructComparison struct {
|
|
Changes []StructDiff `json:"changes"`
|
|
Summary string `json:"summary"`
|
|
}
|
|
|
|
func CompareStructs(oldStruct, newStruct any) StructComparison {
|
|
result := StructComparison{
|
|
Changes: []StructDiff{},
|
|
}
|
|
|
|
if oldStruct == nil && newStruct == nil {
|
|
result.Summary = "Both structs are nil"
|
|
return result
|
|
}
|
|
|
|
if oldStruct == nil {
|
|
result.Summary = "Old struct is nil, new struct has values"
|
|
return result
|
|
}
|
|
|
|
if newStruct == nil {
|
|
result.Summary = "New struct is nil, old struct had values"
|
|
return result
|
|
}
|
|
|
|
oldVal := reflect.ValueOf(oldStruct)
|
|
newVal := reflect.ValueOf(newStruct)
|
|
|
|
if oldVal.Type() != newVal.Type() {
|
|
result.Summary = "Struct types do not match"
|
|
return result
|
|
}
|
|
|
|
if oldVal.Kind() == reflect.Ptr {
|
|
oldVal = oldVal.Elem()
|
|
}
|
|
if newVal.Kind() == reflect.Ptr {
|
|
newVal = newVal.Elem()
|
|
}
|
|
|
|
if oldVal.Kind() != reflect.Struct || newVal.Kind() != reflect.Struct {
|
|
result.Summary = "Both values must be structs"
|
|
return result
|
|
}
|
|
|
|
oldType := oldVal.Type()
|
|
changeCount := 0
|
|
|
|
for i := 0; i < oldVal.NumField(); i++ {
|
|
field := oldType.Field(i)
|
|
|
|
if !field.IsExported() {
|
|
continue
|
|
}
|
|
|
|
oldFieldVal := oldVal.Field(i)
|
|
newFieldVal := newVal.Field(i)
|
|
|
|
if !oldFieldVal.CanInterface() || !newFieldVal.CanInterface() {
|
|
continue
|
|
}
|
|
|
|
oldInterface := oldFieldVal.Interface()
|
|
newInterface := newFieldVal.Interface()
|
|
|
|
if !reflect.DeepEqual(oldInterface, newInterface) {
|
|
result.Changes = append(result.Changes, StructDiff{
|
|
FieldName: field.Name,
|
|
OldValue: oldInterface,
|
|
NewValue: newInterface,
|
|
})
|
|
changeCount++
|
|
}
|
|
}
|
|
|
|
if changeCount == 0 {
|
|
result.Summary = "No changes detected"
|
|
} else if changeCount == 1 {
|
|
result.Summary = "1 field changed"
|
|
} else {
|
|
result.Summary = fmt.Sprintf("%d fields changed", changeCount)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Strips non-numeric characters from a string including spaces.
|
|
func SanitizeNum(input string) string {
|
|
re := regexp.MustCompile(`[^\d]+`)
|
|
return re.ReplaceAllString(input, "")
|
|
}
|
|
|
|
// Strips non-alphanumeric, non-space characters from a string.
|
|
func SanitizeAlphaNum(input string) string {
|
|
re := regexp.MustCompile(`[^a-zA-Z0-9 ]+`)
|
|
return re.ReplaceAllString(input, "")
|
|
}
|
|
|
|
// Strips non-alphanumeric characters from a string including spaces.
|
|
func SanitizeAlphaNumStrict(input string) string {
|
|
re := regexp.MustCompile(`[^a-zA-Z0-9]+`)
|
|
return re.ReplaceAllString(input, "")
|
|
}
|
|
|
|
// MapMerge merges two maps, with values from m2 overriding values from m1.
|
|
func MapMerge[K comparable, V any](m1, m2 map[K]V) map[K]V {
|
|
result := make(map[K]V, len(m1)+len(m2))
|
|
for k, v := range m1 {
|
|
result[k] = v
|
|
}
|
|
for k, v := range m2 {
|
|
result[k] = v
|
|
}
|
|
return result
|
|
}
|