restructure project, add claudemd
This commit is contained in:
643
go/basic/basic.go
Normal file
643
go/basic/basic.go
Normal file
@@ -0,0 +1,643 @@
|
||||
// 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
|
||||
}
|
||||
150
go/basic/basic_test.go
Normal file
150
go/basic/basic_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package basic
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeName(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
// Basic capitalization
|
||||
{"JOHN", "John"},
|
||||
{"doe", "Doe"},
|
||||
{"jOhN", "jOhN"}, // mixed case — trust user input
|
||||
|
||||
// O' prefix
|
||||
{"O'BRIAN", "O'Brian"},
|
||||
{"o'connor", "O'Connor"},
|
||||
{"O'NEIL", "O'Neil"},
|
||||
|
||||
// Mc prefix
|
||||
{"MCDONALD", "McDonald"},
|
||||
{"mcdonald", "McDonald"},
|
||||
{"MCBRIDE", "McBride"},
|
||||
|
||||
// Mac prefix (len >= 5 to avoid false positives)
|
||||
{"MACARTHUR", "MacArthur"},
|
||||
{"macarthur", "MacArthur"},
|
||||
{"MACDONALD", "MacDonald"},
|
||||
|
||||
// Mac prefix - short words should NOT get Mac treatment
|
||||
{"MACK", "Mack"},
|
||||
{"macy", "Macy"},
|
||||
{"mace", "Mace"},
|
||||
{"mach", "Mach"},
|
||||
|
||||
// Hyphenated names
|
||||
{"SMITH-JONES", "Smith-Jones"},
|
||||
{"smith-jones", "Smith-Jones"},
|
||||
{"O'BRIEN-MCDONALD", "O'Brien-McDonald"},
|
||||
|
||||
// Roman numerals and suffixes
|
||||
{"iii", "III"},
|
||||
{"III", "III"},
|
||||
{"iv", "IV"},
|
||||
{"jr", "Jr."},
|
||||
{"sr", "Sr."},
|
||||
{"ii", "II"},
|
||||
|
||||
// Compound surnames (De, Di, La, Le, Lo, Du)
|
||||
{"DESANTIS", "DeSantis"},
|
||||
{"devries", "DeVries"},
|
||||
{"DEMARCO", "DeMarco"},
|
||||
{"DIMAGGIO", "DiMaggio"},
|
||||
{"dicaprio", "DiCaprio"},
|
||||
{"LASALLE", "LaSalle"},
|
||||
{"lafleur", "LaFleur"},
|
||||
{"LEBLANC", "LeBlanc"},
|
||||
{"lebron", "LeBron"},
|
||||
{"LOPRESTI", "LoPresti"},
|
||||
{"DUBOIS", "DuBois"},
|
||||
{"dupont", "DuPont"},
|
||||
|
||||
// Compound surname prefixes should NOT affect regular names
|
||||
{"DEAN", "Dean"},
|
||||
{"DENNIS", "Dennis"},
|
||||
{"DIANA", "Diana"},
|
||||
{"LAURA", "Laura"},
|
||||
{"LEON", "Leon"},
|
||||
|
||||
// Multi-word last names
|
||||
{"DE LA CRUZ", "De La Cruz"},
|
||||
{"VAN DER BERG", "Van Der Berg"},
|
||||
|
||||
// Whitespace handling
|
||||
{" JOHN ", "John"},
|
||||
{"", ""},
|
||||
{" ", ""},
|
||||
|
||||
// Mixed case — skip normalization, trust user input
|
||||
{"John", "John"},
|
||||
{"Smith", "Smith"},
|
||||
{"DeSantis", "DeSantis"},
|
||||
{"DiCaprio", "DiCaprio"},
|
||||
{"O'Brien", "O'Brien"},
|
||||
{"LeBron", "LeBron"},
|
||||
{"MacArthur", "MacArthur"},
|
||||
{"McDonald", "McDonald"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
result := NormalizeName(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("NormalizeName(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNameContainsProfanity(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
// Should flag
|
||||
{"fuck", true},
|
||||
{"FUCK", true},
|
||||
{"Shit", true},
|
||||
{"ass", true},
|
||||
|
||||
// Should NOT flag — profane substrings inside real names
|
||||
{"Massimo", false},
|
||||
{"Dickens", false},
|
||||
{"Cockburn", false},
|
||||
{"Draper", false},
|
||||
{"Ashton", false},
|
||||
{"Cassidy", false},
|
||||
|
||||
// Normal names
|
||||
{"John", false},
|
||||
{"Smith", false},
|
||||
{"O'Brien", false},
|
||||
|
||||
// Profanity in hyphenated or multi-word name
|
||||
{"Fuck-Face", true},
|
||||
{"Dick Head", true},
|
||||
|
||||
// Leet speak
|
||||
{"b1tch", true},
|
||||
{"a$$", true},
|
||||
{"sh!t", true},
|
||||
{"fvck", false}, // not in leet map, won't match
|
||||
{"f4g", true},
|
||||
{"4ss", true},
|
||||
{"d1ck", true},
|
||||
{"pu$$y", true},
|
||||
|
||||
// Empty
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
result := NameContainsProfanity(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("NameContainsProfanity(%q) = %v, want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user