Files
kjol/basic/basic.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
}