This commit is contained in:
Eden Kirin
2025-10-31 19:04:40 +01:00
parent ca10b01fb0
commit 4e4827d640
12 changed files with 2612 additions and 10 deletions

View File

@ -155,8 +155,12 @@ func Pluralize(word string) string {
return preserveCase(word, plural)
}
// Already plural (ends in 's' and not special case)
if strings.HasSuffix(lower, "s") && !strings.HasSuffix(lower, "us") {
// Already plural (ends in 's' after a consonant, but not 'ss', 'us', 'is')
// Skip this check if word ends in 'ss' (like 'class', 'glass')
if strings.HasSuffix(lower, "s") &&
!strings.HasSuffix(lower, "ss") &&
!strings.HasSuffix(lower, "us") &&
!strings.HasSuffix(lower, "is") {
return word
}

View File

@ -0,0 +1,247 @@
package naming
import (
"testing"
)
func TestSingularize(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
// Regular plurals
{"simple s", "users", "user"},
{"simple s uppercase", "Users", "User"},
{"tables", "tables", "table"},
// ies -> y
{"ies to y", "companies", "company"},
{"ies to y uppercase", "Companies", "Company"},
{"cities", "cities", "city"},
{"categories", "categories", "category"},
// ves -> f/fe
{"ves to f", "halves", "half"},
{"ves to fe - knife", "knives", "knife"},
{"ves to fe - wife", "wives", "wife"},
{"ves to fe - life", "lives", "life"},
// es endings
{"xes", "boxes", "box"},
{"shes", "dishes", "dish"},
{"ches", "watches", "watch"},
{"ses", "classes", "class"},
// oes -> o
{"oes", "tomatoes", "tomato"},
{"heroes", "heroes", "hero"},
// Irregular plurals
{"people", "people", "person"},
{"children", "children", "child"},
{"men", "men", "man"},
{"women", "women", "woman"},
{"teeth", "teeth", "tooth"},
{"feet", "feet", "foot"},
{"mice", "mice", "mouse"},
{"geese", "geese", "goose"},
// Unchanged
{"sheep", "sheep", "sheep"},
{"fish", "fish", "fish"},
{"deer", "deer", "deer"},
{"series", "series", "series"},
{"species", "species", "species"},
// Special cases
{"data", "data", "datum"},
{"analyses", "analyses", "analysis"},
{"crises", "crises", "crisis"},
// Edge cases
{"empty", "", ""},
{"already singular", "user", "user"},
{"ss ending", "glass", "glass"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Singularize(tt.input)
if result != tt.expected {
t.Errorf("Singularize(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
func TestPluralize(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
// Regular plurals
{"simple", "user", "users"},
{"simple uppercase", "User", "Users"},
{"table", "table", "tables"},
// y -> ies
{"y to ies", "company", "companies"},
{"y to ies uppercase", "Company", "Companies"},
{"city", "city", "cities"},
{"category", "category", "categories"},
// consonant + y stays
{"ay ending", "day", "days"},
{"ey ending", "key", "keys"},
{"oy ending", "boy", "boys"},
// f/fe -> ves
{"f to ves", "half", "halves"},
{"fe to ves - knife", "knife", "knives"},
{"fe to ves - wife", "wife", "wives"},
{"fe to ves - life", "life", "lives"},
// s, x, sh, ch -> es
{"x to es", "box", "boxes"},
{"sh to es", "dish", "dishes"},
{"ch to es", "watch", "watches"},
{"s to es", "class", "classes"},
// o -> oes
{"o to oes", "tomato", "tomatoes"},
{"hero", "hero", "heroes"},
// Irregular plurals
{"person", "person", "people"},
{"child", "child", "children"},
{"man", "man", "men"},
{"woman", "woman", "women"},
{"tooth", "tooth", "teeth"},
{"foot", "foot", "feet"},
{"mouse", "mouse", "mice"},
{"goose", "goose", "geese"},
// Unchanged
{"sheep", "sheep", "sheep"},
{"fish", "fish", "fish"},
{"deer", "deer", "deer"},
{"series", "series", "series"},
{"species", "species", "species"},
// Edge cases
{"empty", "", ""},
{"already plural", "users", "users"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Pluralize(tt.input)
if result != tt.expected {
t.Errorf("Pluralize(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
func TestSingularizeTableName(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"simple", "users", "user"},
{"compound", "user_accounts", "user_account"},
{"triple compound", "user_login_histories", "user_login_history"},
{"already singular", "user", "user"},
{"ies ending", "cashbag_conforms", "cashbag_conform"},
{"complex table", "auth_user_groups", "auth_user_group"},
{"empty", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := SingularizeTableName(tt.input)
if result != tt.expected {
t.Errorf("SingularizeTableName(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
func TestToPascalCase(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"simple", "user", "User"},
{"snake_case", "user_account", "UserAccount"},
{"triple", "user_login_history", "UserLoginHistory"},
{"already pascal", "UserAccount", "UserAccount"},
{"single char", "a", "A"},
{"empty", "", ""},
{"with numbers", "user_2fa", "User2fa"},
{"multiple underscores", "user___account", "UserAccount"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ToPascalCase(tt.input)
if result != tt.expected {
t.Errorf("ToPascalCase(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
func TestToSnakeCase(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"PascalCase", "UserAccount", "user_account"},
{"simple", "User", "user"},
{"multiple words", "UserLoginHistory", "user_login_history"},
{"already snake", "user_account", "user_account"},
{"empty", "", ""},
{"single char", "A", "a"},
{"with numbers", "User2FA", "user2_f_a"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ToSnakeCase(tt.input)
if result != tt.expected {
t.Errorf("ToSnakeCase(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
// Benchmark tests
func BenchmarkSingularize(b *testing.B) {
for i := 0; i < b.N; i++ {
Singularize("companies")
}
}
func BenchmarkPluralize(b *testing.B) {
for i := 0; i < b.N; i++ {
Pluralize("company")
}
}
func BenchmarkToPascalCase(b *testing.B) {
for i := 0; i < b.N; i++ {
ToPascalCase("user_login_history")
}
}
func BenchmarkToSnakeCase(b *testing.B) {
for i := 0; i < b.N; i++ {
ToSnakeCase("UserLoginHistory")
}
}