From e0f91ec47716069cccd8eb324dd8b57effdc328e Mon Sep 17 00:00:00 2001 From: Stanislas Michalak Date: Sun, 28 Jun 2026 11:23:21 +0200 Subject: [PATCH 1/4] chore: break name package dep on root package - Introduce an internal package to share core functions - Avoid any dep from inner packages to outer / root packages - Fix performance / allocation issues (validated with benchmarks) --- SHOULDERS.md | 12 - acronyms.go | 155 +--------- bench_test.go | 84 ++++++ camelize.go | 38 +-- camelize_test.go | 4 +- capitalize.go | 18 +- capitalize_test.go | 4 +- custom_data.go | 85 +----- dasherize.go | 28 +- dasherize_test.go | 4 +- flect.go | 39 --- flect_test.go | 26 +- go.mod | 6 +- go.sum | 13 - humanize.go | 24 +- humanize_test.go | 4 +- ident.go | 117 +------- ident_test.go | 6 +- internal/core/acronyms.go | 152 ++++++++++ internal/core/camelize.go | 46 +++ internal/core/capitalize.go | 26 ++ internal/core/core.go | 34 +++ internal/core/custom_data.go | 87 ++++++ internal/core/dasherize.go | 36 +++ internal/core/humanize.go | 36 +++ internal/core/ident.go | 110 +++++++ internal/core/lower_upper.go | 13 + internal/core/ordinalize.go | 41 +++ internal/core/pascalize.go | 34 +++ internal/core/plural_rules.go | 430 ++++++++++++++++++++++++++++ internal/core/pluralize.go | 81 ++++++ internal/core/rule.go | 17 ++ internal/core/singular_rules.go | 26 ++ internal/core/singularize.go | 78 +++++ internal/core/titleize.go | 40 +++ internal/core/underscore.go | 37 +++ internal/testhelpers/testhelpers.go | 59 ++++ lower_upper.go | 12 - name/char_test.go | 4 +- name/file.go | 6 +- name/file_test.go | 4 +- name/folder.go | 6 +- name/folder_test.go | 4 +- name/ident.go | 6 +- name/interface.go | 2 +- name/interface_test.go | 6 +- name/join_test.go | 4 +- name/key_test.go | 4 +- name/name.go | 8 +- name/name_test.go | 8 +- name/os_path_test.go | 4 +- name/package_test.go | 4 +- name/param_id_test.go | 4 +- name/resource_test.go | 4 +- name/tablize_test.go | 4 +- name/testhelpers_test.go | 13 + name/url_test.go | 4 +- name/var_case_test.go | 8 +- ordinalize.go | 37 +-- ordinalize_test.go | 4 +- pascalize.go | 26 +- pascalize_test.go | 4 +- plural_rules.go | 408 +------------------------- pluralize.go | 64 +---- pluralize_test.go | 6 +- rule.go | 16 -- singular_rules.go | 17 +- singularize.go | 61 +--- singularize_test.go | 6 +- testhelpers_test.go | 13 + titleize.go | 32 +-- titleize_test.go | 4 +- underscore.go | 29 +- underscore_test.go | 4 +- 74 files changed, 1616 insertions(+), 1284 deletions(-) delete mode 100644 SHOULDERS.md create mode 100644 bench_test.go create mode 100644 internal/core/acronyms.go create mode 100644 internal/core/camelize.go create mode 100644 internal/core/capitalize.go create mode 100644 internal/core/core.go create mode 100644 internal/core/custom_data.go create mode 100644 internal/core/dasherize.go create mode 100644 internal/core/humanize.go create mode 100644 internal/core/ident.go create mode 100644 internal/core/lower_upper.go create mode 100644 internal/core/ordinalize.go create mode 100644 internal/core/pascalize.go create mode 100644 internal/core/plural_rules.go create mode 100644 internal/core/pluralize.go create mode 100644 internal/core/rule.go create mode 100644 internal/core/singular_rules.go create mode 100644 internal/core/singularize.go create mode 100644 internal/core/titleize.go create mode 100644 internal/core/underscore.go create mode 100644 internal/testhelpers/testhelpers.go create mode 100644 name/testhelpers_test.go create mode 100644 testhelpers_test.go diff --git a/SHOULDERS.md b/SHOULDERS.md deleted file mode 100644 index 75222c6..0000000 --- a/SHOULDERS.md +++ /dev/null @@ -1,12 +0,0 @@ -# Flect Stands on the Shoulders of Giants - -Flect does not try to reinvent the wheel! Instead, it uses the already great wheels developed by the Go community and puts them all together in the best way possible. Without these giants, this project would not be possible. Please make sure to check them out and thank them for all of their hard work. - -Thank you to the following **GIANTS**: - -* [github.com/davecgh/go-spew](https://godoc.org/github.com/davecgh/go-spew) -* [github.com/pmezard/go-difflib](https://godoc.org/github.com/pmezard/go-difflib) -* [github.com/stretchr/objx](https://godoc.org/github.com/stretchr/objx) -* [github.com/stretchr/testify](https://godoc.org/github.com/stretchr/testify) -* [gopkg.in/check.v1](https://godoc.org/gopkg.in/check.v1) -* [gopkg.in/yaml.v3](https://godoc.org/gopkg.in/yaml.v3) diff --git a/acronyms.go b/acronyms.go index b169724..b1da380 100644 --- a/acronyms.go +++ b/acronyms.go @@ -1,152 +1,9 @@ package flect -import "sync" +import "github.com/gobuffalo/flect/internal/core" -var acronymsMoot = &sync.RWMutex{} - -var baseAcronyms = map[string]bool{ - "OK": true, - "UTF8": true, - "HTML": true, - "JSON": true, - "JWT": true, - "ID": true, - "UUID": true, - "SQL": true, - "ACK": true, - "ACL": true, - "ADSL": true, - "AES": true, - "ANSI": true, - "API": true, - "ARP": true, - "ATM": true, - "BGP": true, - "BSS": true, - "CCITT": true, - "CHAP": true, - "CIDR": true, - "CIR": true, - "CLI": true, - "CPE": true, - "CPU": true, - "CRC": true, - "CRT": true, - "CSMA": true, - "CMOS": true, - "DCE": true, - "DEC": true, - "DES": true, - "DHCP": true, - "DNS": true, - "DRAM": true, - "DSL": true, - "DSLAM": true, - "DTE": true, - "DMI": true, - "EHA": true, - "EIA": true, - "EIGRP": true, - "EOF": true, - "ESS": true, - "FCC": true, - "FCS": true, - "FDDI": true, - "FTP": true, - "GBIC": true, - "gbps": true, - "GEPOF": true, - "HDLC": true, - "HTTP": true, - "HTTPS": true, - "IANA": true, - "ICMP": true, - "IDF": true, - "IDS": true, - "IEEE": true, - "IETF": true, - "IMAP": true, - "IP": true, - "IPS": true, - "ISDN": true, - "ISP": true, - "kbps": true, - "LACP": true, - "LAN": true, - "LAPB": true, - "LAPF": true, - "LLC": true, - "MAC": true, - "Mbps": true, - "MC": true, - "MDF": true, - "MIB": true, - "MoCA": true, - "MPLS": true, - "MTU": true, - "NAC": true, - "NAT": true, - "NBMA": true, - "NIC": true, - "NRZ": true, - "NRZI": true, - "NVRAM": true, - "OSI": true, - "OSPF": true, - "OUI": true, - "PAP": true, - "PAT": true, - "PC": true, - "PIM": true, - "PCM": true, - "PDU": true, - "POP3": true, - "POTS": true, - "PPP": true, - "PPTP": true, - "PTT": true, - "PVST": true, - "RAM": true, - "RARP": true, - "RFC": true, - "RIP": true, - "RLL": true, - "ROM": true, - "RSTP": true, - "RTP": true, - "RCP": true, - "SDLC": true, - "SFD": true, - "SFP": true, - "SLARP": true, - "SLIP": true, - "SMTP": true, - "SNA": true, - "SNAP": true, - "SNMP": true, - "SOF": true, - "SRAM": true, - "SSH": true, - "SSID": true, - "STP": true, - "SYN": true, - "TDM": true, - "TFTP": true, - "TIA": true, - "TOFU": true, - "UDP": true, - "URL": true, - "URI": true, - "USB": true, - "UTP": true, - "VC": true, - "VLAN": true, - "VLSM": true, - "VPN": true, - "W3C": true, - "WAN": true, - "WEP": true, - "WiFi": true, - "WPA": true, - "WWW": true, -} +// baseAcronyms and dictionary are the same references as in internal/core, +// kept here so that package-level tests in this package can access them +// without importing the internal package directly. +var baseAcronyms = core.BaseAcronyms +var dictionary = core.Dictionary diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 0000000..eb93664 --- /dev/null +++ b/bench_test.go @@ -0,0 +1,84 @@ +package flect + +import "testing" + +// benchInputs exercises the key code paths: acronym fast-path, camelCase +// splitting, underscore-separated words, multi-segment paths, and plain words. +var benchInputs = []string{ + "widget", + "widget_id", + "WidgetID", + "Widget_ID", + "HTMLParser", + "JSONResponse", + "admin/widget", + "bob dylan", + "Nice to see you!", + "ThisIsALongCamelCaseString", + "JWTName", + "foo_bar_baz", +} + +func BenchmarkCamelize(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + for _, s := range benchInputs { + Camelize(s) + } + } +} + +func BenchmarkPascalize(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + for _, s := range benchInputs { + Pascalize(s) + } + } +} + +func BenchmarkDasherize(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + for _, s := range benchInputs { + Dasherize(s) + } + } +} + +func BenchmarkTitleize(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + for _, s := range benchInputs { + Titleize(s) + } + } +} + +func BenchmarkHumanize(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + for _, s := range benchInputs { + Humanize(s) + } + } +} + +func BenchmarkCapitalize(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + for _, s := range benchInputs { + Capitalize(s) + } + } +} + +func BenchmarkOrdinalize(b *testing.B) { + b.ReportAllocs() + inputs := []string{"1", "2", "3", "11", "12", "13", "21", "42", "100", "101", "111", "1001"} + for n := 0; n < b.N; n++ { + for _, s := range inputs { + Ordinalize(s) + } + } +} diff --git a/camelize.go b/camelize.go index d8851c8..e3db062 100644 --- a/camelize.go +++ b/camelize.go @@ -1,44 +1,12 @@ package flect -import ( - "strings" - "unicode" -) +import "github.com/gobuffalo/flect/internal/core" // Camelize returns a camelize version of a string +// // bob dylan = bobDylan // widget_id = widgetID // WidgetID = widgetID func Camelize(s string) string { - return New(s).Camelize().String() -} - -// Camelize returns a camelize version of a string -// bob dylan = bobDylan -// widget_id = widgetID -// WidgetID = widgetID -func (i Ident) Camelize() Ident { - var out []string - for i, part := range i.Parts { - var x string - var capped bool - for _, c := range part { - if unicode.IsLetter(c) || unicode.IsDigit(c) { - if i == 0 { - x += string(unicode.ToLower(c)) - continue - } - if !capped { - capped = true - x += string(unicode.ToUpper(c)) - continue - } - x += string(c) - } - } - if x != "" { - out = append(out, x) - } - } - return New(strings.Join(out, "")) + return core.Camelize(s) } diff --git a/camelize_test.go b/camelize_test.go index 8f546ed..cfde70e 100644 --- a/camelize_test.go +++ b/camelize_test.go @@ -2,8 +2,6 @@ package flect import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_Camelize(t *testing.T) { @@ -36,7 +34,7 @@ func Test_Camelize(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, Camelize(tt.act)) r.Equal(tt.exp, Camelize(tt.exp)) }) diff --git a/capitalize.go b/capitalize.go index 78334fc..45c00f2 100644 --- a/capitalize.go +++ b/capitalize.go @@ -1,24 +1,12 @@ package flect -import "unicode" +import "github.com/gobuffalo/flect/internal/core" // Capitalize will cap the first letter of string +// // user = User // bob dylan = Bob dylan // widget_id = Widget_id func Capitalize(s string) string { - return New(s).Capitalize().String() -} - -// Capitalize will cap the first letter of string -// user = User -// bob dylan = Bob dylan -// widget_id = Widget_id -func (i Ident) Capitalize() Ident { - if len(i.Parts) == 0 { - return New("") - } - runes := []rune(i.Original) - runes[0] = unicode.ToTitle(runes[0]) - return New(string(runes)) + return core.Capitalize(s) } diff --git a/capitalize_test.go b/capitalize_test.go index ac8650f..868c499 100644 --- a/capitalize_test.go +++ b/capitalize_test.go @@ -2,8 +2,6 @@ package flect import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_Capitalize(t *testing.T) { @@ -20,7 +18,7 @@ func Test_Capitalize(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, Capitalize(tt.act)) r.Equal(tt.exp, Capitalize(tt.exp)) }) diff --git a/custom_data.go b/custom_data.go index efb445f..79707dc 100644 --- a/custom_data.go +++ b/custom_data.go @@ -1,88 +1,21 @@ package flect import ( - "bytes" - "encoding/json" - "fmt" "io" - "io/ioutil" - "os" - "path/filepath" - "strings" -) - -func init() { - loadCustomData("inflections.json", "INFLECT_PATH", "could not read inflection file", LoadInflections) - loadCustomData("acronyms.json", "ACRONYMS_PATH", "could not read acronyms file", LoadAcronyms) -} - -//CustomDataParser are functions that parse data like acronyms or -//plurals in the shape of a io.Reader it receives. -type CustomDataParser func(io.Reader) error -func loadCustomData(defaultFile, env, readErrorMessage string, parser CustomDataParser) { - pwd, _ := os.Getwd() - path, found := os.LookupEnv(env) - if !found { - path = filepath.Join(pwd, defaultFile) - } - - if _, err := os.Stat(path); err != nil { - return - } - - b, err := ioutil.ReadFile(path) - if err != nil { - fmt.Printf("%s %s (%s)\n", readErrorMessage, path, err) - return - } + "github.com/gobuffalo/flect/internal/core" +) - if err = parser(bytes.NewReader(b)); err != nil { - fmt.Println(err) - } -} +// CustomDataParser are functions that parse data like acronyms or +// plurals in the shape of a io.Reader it receives. +type CustomDataParser = core.CustomDataParser -//LoadAcronyms loads rules from io.Reader param +// LoadAcronyms loads acronyms from io.Reader param func LoadAcronyms(r io.Reader) error { - m := []string{} - err := json.NewDecoder(r).Decode(&m) - - if err != nil { - return fmt.Errorf("could not decode acronyms JSON from reader: %s", err) - } - - acronymsMoot.Lock() - defer acronymsMoot.Unlock() - - for _, acronym := range m { - baseAcronyms[acronym] = true - } - - return nil + return core.LoadAcronyms(r) } -//LoadInflections loads rules from io.Reader param +// LoadInflections loads rules from io.Reader param func LoadInflections(r io.Reader) error { - m := map[string]string{} - - err := json.NewDecoder(r).Decode(&m) - if err != nil { - return fmt.Errorf("could not decode inflection JSON from reader: %s", err) - } - - pluralMoot.Lock() - defer pluralMoot.Unlock() - singularMoot.Lock() - defer singularMoot.Unlock() - - for s, p := range m { - if strings.Contains(s, " ") || strings.Contains(p, " ") { - // flect works with parts, so multi-words should not be allowed - return fmt.Errorf("inflection elements should be a single word") - } - singleToPlural[s] = p - pluralToSingle[p] = s - } - - return nil + return core.LoadInflections(r) } diff --git a/dasherize.go b/dasherize.go index c7a8a33..12ea0a7 100644 --- a/dasherize.go +++ b/dasherize.go @@ -1,34 +1,12 @@ package flect -import ( - "strings" - "unicode" -) +import "github.com/gobuffalo/flect/internal/core" // Dasherize returns an alphanumeric, lowercased, dashed string +// // Donald E. Knuth = donald-e-knuth // Test with + sign = test-with-sign // admin/WidgetID = admin-widget-id func Dasherize(s string) string { - return New(s).Dasherize().String() -} - -// Dasherize returns an alphanumeric, lowercased, dashed string -// Donald E. Knuth = donald-e-knuth -// Test with + sign = test-with-sign -// admin/WidgetID = admin-widget-id -func (i Ident) Dasherize() Ident { - var parts []string - - for _, part := range i.Parts { - var x string - for _, c := range part { - if unicode.IsLetter(c) || unicode.IsDigit(c) { - x += string(c) - } - } - parts = xappend(parts, x) - } - - return New(strings.ToLower(strings.Join(parts, "-"))) + return core.Dasherize(s) } diff --git a/dasherize_test.go b/dasherize_test.go index 7d754c0..84cbfd5 100644 --- a/dasherize_test.go +++ b/dasherize_test.go @@ -2,8 +2,6 @@ package flect import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_Dasherize(t *testing.T) { @@ -21,7 +19,7 @@ func Test_Dasherize(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, Dasherize(tt.act)) r.Equal(tt.exp, Dasherize(tt.exp)) }) diff --git a/flect.go b/flect.go index ee81b6f..a530417 100644 --- a/flect.go +++ b/flect.go @@ -2,42 +2,3 @@ Package flect is a new inflection engine to replace [https://github.com/markbates/inflect](https://github.com/markbates/inflect) designed to be more modular, more readable, and easier to fix issues on than the original. */ package flect - -import ( - "strings" - "unicode" -) - -var spaces = []rune{'_', ' ', ':', '-', '/'} - -func isSpace(c rune) bool { - for _, r := range spaces { - if r == c { - return true - } - } - return unicode.IsSpace(c) -} - -func xappend(a []string, ss ...string) []string { - for _, s := range ss { - s = strings.TrimSpace(s) - for _, x := range spaces { - s = strings.Trim(s, string(x)) - } - if _, ok := baseAcronyms[strings.ToUpper(s)]; ok { - s = strings.ToUpper(s) - } - if s != "" { - a = append(a, s) - } - } - return a -} - -func abs(x int) int { - if x < 0 { - return -x - } - return x -} diff --git a/flect_test.go b/flect_test.go index 4ab6251..44477f0 100644 --- a/flect_test.go +++ b/flect_test.go @@ -4,8 +4,6 @@ import ( "bytes" "encoding/json" "testing" - - "github.com/stretchr/testify/require" ) type tt struct { @@ -14,7 +12,7 @@ type tt struct { } func Test_LoadInflections(t *testing.T) { - r := require.New(t) + r := newRequire(t) m := map[string]string{ "baby": "bebe", "xyz": "zyx", @@ -34,7 +32,7 @@ func Test_LoadInflections(t *testing.T) { } func Test_LoadInflectionsWrongSingular(t *testing.T) { - r := require.New(t) + r := newRequire(t) m := map[string]string{ "a file": "files", } @@ -46,7 +44,7 @@ func Test_LoadInflectionsWrongSingular(t *testing.T) { } func Test_LoadInflectionsWrongPlural(t *testing.T) { - r := require.New(t) + r := newRequire(t) m := map[string]string{ "beatle": "the beatles", } @@ -58,7 +56,7 @@ func Test_LoadInflectionsWrongPlural(t *testing.T) { } func Test_LoadAcronyms(t *testing.T) { - r := require.New(t) + r := newRequire(t) m := []string{ "ACC", "TLC", @@ -348,20 +346,20 @@ var singlePluralAssertions = []dict{ func init() { for _, wd := range dictionary { - if wd.uncountable && wd.plural == "" { - wd.plural = wd.singular + if wd.Uncountable && wd.Plural == "" { + wd.Plural = wd.Singular } singlePluralAssertions = append(singlePluralAssertions, dict{ - singular: wd.singular, - plural: wd.plural, - doSingularizeTest: !wd.unidirectional, + singular: wd.Singular, + plural: wd.Plural, + doSingularizeTest: !wd.Unidirectional, }) - if wd.alternative != "" { + if wd.Alternative != "" { singlePluralAssertions = append(singlePluralAssertions, dict{ - singular: wd.singular, - plural: wd.alternative, + singular: wd.Singular, + plural: wd.Alternative, doPluralizeTest: false, }) } diff --git a/go.mod b/go.mod index 66bab2d..f408a55 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,5 @@ module github.com/gobuffalo/flect -go 1.16 - -exclude github.com/stretchr/testify v1.7.1 - -require github.com/stretchr/testify v1.8.1 +go 1.20 retract [v1.0.0, v1.0.1] diff --git a/go.sum b/go.sum index 4e4b32a..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +0,0 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/humanize.go b/humanize.go index 5100bfb..6fe9511 100644 --- a/humanize.go +++ b/humanize.go @@ -1,8 +1,6 @@ package flect -import ( - "strings" -) +import "github.com/gobuffalo/flect/internal/core" // Humanize returns first letter of sentence capitalized. // Common acronyms are capitalized as well. @@ -14,23 +12,5 @@ import ( // first_Name = First Name // firstName = First Name func Humanize(s string) string { - return New(s).Humanize().String() -} - -// Humanize First letter of sentence capitalized -func (i Ident) Humanize() Ident { - if len(i.Original) == 0 { - return New("") - } - - if strings.TrimSpace(i.Original) == "" { - return i - } - - parts := xappend([]string{}, Titleize(i.Parts[0])) - if len(i.Parts) > 1 { - parts = xappend(parts, i.Parts[1:]...) - } - - return New(strings.Join(parts, " ")) + return core.Humanize(s) } diff --git a/humanize_test.go b/humanize_test.go index 2c97a61..b590cce 100644 --- a/humanize_test.go +++ b/humanize_test.go @@ -2,8 +2,6 @@ package flect import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_Humanize(t *testing.T) { @@ -30,7 +28,7 @@ func Test_Humanize(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, Humanize(tt.act)) r.Equal(tt.exp, Humanize(tt.exp)) }) diff --git a/ident.go b/ident.go index 9189e9a..91cb26d 100644 --- a/ident.go +++ b/ident.go @@ -1,122 +1,11 @@ package flect -import ( - "encoding" - "strings" - "unicode" - "unicode/utf8" -) +import "github.com/gobuffalo/flect/internal/core" // Ident represents the string and it's parts -type Ident struct { - Original string - Parts []string -} - -// String implements fmt.Stringer and returns the original string -func (i Ident) String() string { - return i.Original -} +type Ident = core.Ident // New creates a new Ident from the string func New(s string) Ident { - i := Ident{ - Original: s, - Parts: toParts(s), - } - - return i -} - -func toParts(s string) []string { - parts := []string{} - s = strings.TrimSpace(s) - if len(s) == 0 { - return parts - } - if _, ok := baseAcronyms[strings.ToUpper(s)]; ok { - return []string{strings.ToUpper(s)} - } - var prev rune - var x strings.Builder - x.Grow(len(s)) - for _, c := range s { - // fmt.Println("### cs ->", cs) - // fmt.Println("### unicode.IsControl(c) ->", unicode.IsControl(c)) - // fmt.Println("### unicode.IsDigit(c) ->", unicode.IsDigit(c)) - // fmt.Println("### unicode.IsGraphic(c) ->", unicode.IsGraphic(c)) - // fmt.Println("### unicode.IsLetter(c) ->", unicode.IsLetter(c)) - // fmt.Println("### unicode.IsLower(c) ->", unicode.IsLower(c)) - // fmt.Println("### unicode.IsMark(c) ->", unicode.IsMark(c)) - // fmt.Println("### unicode.IsPrint(c) ->", unicode.IsPrint(c)) - // fmt.Println("### unicode.IsPunct(c) ->", unicode.IsPunct(c)) - // fmt.Println("### unicode.IsSpace(c) ->", unicode.IsSpace(c)) - // fmt.Println("### unicode.IsTitle(c) ->", unicode.IsTitle(c)) - // fmt.Println("### unicode.IsUpper(c) ->", unicode.IsUpper(c)) - if !utf8.ValidRune(c) { - continue - } - - if isSpace(c) { - parts = xappend(parts, x.String()) - x.Reset() - x.WriteRune(c) - prev = c - continue - } - - if unicode.IsUpper(c) && !unicode.IsUpper(prev) { - parts = xappend(parts, x.String()) - x.Reset() - x.WriteRune(c) - prev = c - continue - } - if unicode.IsUpper(c) && baseAcronyms[strings.ToUpper(x.String())] { - parts = xappend(parts, x.String()) - x.Reset() - x.WriteRune(c) - prev = c - continue - } - if unicode.IsLetter(c) || unicode.IsDigit(c) || unicode.IsPunct(c) || c == '`' { - prev = c - x.WriteRune(c) - continue - } - - parts = xappend(parts, x.String()) - x.Reset() - prev = c - } - parts = xappend(parts, x.String()) - - return parts -} - -var _ encoding.TextUnmarshaler = &Ident{} -var _ encoding.TextMarshaler = &Ident{} - -// LastPart returns the last part/word of the original string -func (i *Ident) LastPart() string { - if len(i.Parts) == 0 { - return "" - } - return i.Parts[len(i.Parts)-1] -} - -// ReplaceSuffix creates a new Ident with the original suffix replaced by new -func (i Ident) ReplaceSuffix(orig, new string) Ident { - return New(strings.TrimSuffix(i.Original, orig) + new) -} - -//UnmarshalText unmarshalls byte array into the Ident -func (i *Ident) UnmarshalText(data []byte) error { - (*i) = New(string(data)) - return nil -} - -//MarshalText marshals Ident into byte array -func (i Ident) MarshalText() ([]byte, error) { - return []byte(i.Original), nil + return core.New(s) } diff --git a/ident_test.go b/ident_test.go index 3994d29..1fa178e 100644 --- a/ident_test.go +++ b/ident_test.go @@ -2,8 +2,6 @@ package flect import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_New(t *testing.T) { @@ -46,7 +44,7 @@ func Test_New(t *testing.T) { for _, tt := range table { t.Run(tt.Original, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) i := New(tt.Original) r.Equal(tt.Original, i.Original) r.Equal(tt.Parts, i.Parts) @@ -54,7 +52,7 @@ func Test_New(t *testing.T) { } } func Test_MarshalText(t *testing.T) { - r := require.New(t) + r := newRequire(t) n := New("mark") b, err := n.MarshalText() diff --git a/internal/core/acronyms.go b/internal/core/acronyms.go new file mode 100644 index 0000000..f3ede5e --- /dev/null +++ b/internal/core/acronyms.go @@ -0,0 +1,152 @@ +package core + +import "sync" + +var acronymsMoot = &sync.RWMutex{} + +var BaseAcronyms = map[string]bool{ + "OK": true, + "UTF8": true, + "HTML": true, + "JSON": true, + "JWT": true, + "ID": true, + "UUID": true, + "SQL": true, + "ACK": true, + "ACL": true, + "ADSL": true, + "AES": true, + "ANSI": true, + "API": true, + "ARP": true, + "ATM": true, + "BGP": true, + "BSS": true, + "CCITT": true, + "CHAP": true, + "CIDR": true, + "CIR": true, + "CLI": true, + "CPE": true, + "CPU": true, + "CRC": true, + "CRT": true, + "CSMA": true, + "CMOS": true, + "DCE": true, + "DEC": true, + "DES": true, + "DHCP": true, + "DNS": true, + "DRAM": true, + "DSL": true, + "DSLAM": true, + "DTE": true, + "DMI": true, + "EHA": true, + "EIA": true, + "EIGRP": true, + "EOF": true, + "ESS": true, + "FCC": true, + "FCS": true, + "FDDI": true, + "FTP": true, + "GBIC": true, + "gbps": true, + "GEPOF": true, + "HDLC": true, + "HTTP": true, + "HTTPS": true, + "IANA": true, + "ICMP": true, + "IDF": true, + "IDS": true, + "IEEE": true, + "IETF": true, + "IMAP": true, + "IP": true, + "IPS": true, + "ISDN": true, + "ISP": true, + "kbps": true, + "LACP": true, + "LAN": true, + "LAPB": true, + "LAPF": true, + "LLC": true, + "MAC": true, + "Mbps": true, + "MC": true, + "MDF": true, + "MIB": true, + "MoCA": true, + "MPLS": true, + "MTU": true, + "NAC": true, + "NAT": true, + "NBMA": true, + "NIC": true, + "NRZ": true, + "NRZI": true, + "NVRAM": true, + "OSI": true, + "OSPF": true, + "OUI": true, + "PAP": true, + "PAT": true, + "PC": true, + "PIM": true, + "PCM": true, + "PDU": true, + "POP3": true, + "POTS": true, + "PPP": true, + "PPTP": true, + "PTT": true, + "PVST": true, + "RAM": true, + "RARP": true, + "RFC": true, + "RIP": true, + "RLL": true, + "ROM": true, + "RSTP": true, + "RTP": true, + "RCP": true, + "SDLC": true, + "SFD": true, + "SFP": true, + "SLARP": true, + "SLIP": true, + "SMTP": true, + "SNA": true, + "SNAP": true, + "SNMP": true, + "SOF": true, + "SRAM": true, + "SSH": true, + "SSID": true, + "STP": true, + "SYN": true, + "TDM": true, + "TFTP": true, + "TIA": true, + "TOFU": true, + "UDP": true, + "URL": true, + "URI": true, + "USB": true, + "UTP": true, + "VC": true, + "VLAN": true, + "VLSM": true, + "VPN": true, + "W3C": true, + "WAN": true, + "WEP": true, + "WiFi": true, + "WPA": true, + "WWW": true, +} diff --git a/internal/core/camelize.go b/internal/core/camelize.go new file mode 100644 index 0000000..d65a46c --- /dev/null +++ b/internal/core/camelize.go @@ -0,0 +1,46 @@ +package core + +import ( + "strings" + "unicode" +) + +// Camelize returns a camelize version of a string +// +// bob dylan = bobDylan +// widget_id = widgetID +// WidgetID = widgetID +func Camelize(s string) string { + return New(s).Camelize().String() +} + +// Camelize returns a camelize version of a string +// +// bob dylan = bobDylan +// widget_id = widgetID +// WidgetID = widgetID +func (i Ident) Camelize() Ident { + var out []string + for i, part := range i.Parts { + var x strings.Builder + var capped bool + for _, c := range part { + if unicode.IsLetter(c) || unicode.IsDigit(c) { + if i == 0 { + x.WriteRune(unicode.ToLower(c)) + continue + } + if !capped { + capped = true + x.WriteRune(unicode.ToUpper(c)) + continue + } + x.WriteRune(c) + } + } + if x.Len() > 0 { + out = append(out, x.String()) + } + } + return Ident{Original: strings.Join(out, "")} +} diff --git a/internal/core/capitalize.go b/internal/core/capitalize.go new file mode 100644 index 0000000..08a0461 --- /dev/null +++ b/internal/core/capitalize.go @@ -0,0 +1,26 @@ +package core + +import "unicode" + +// Capitalize will cap the first letter of string +// +// user = User +// bob dylan = Bob dylan +// widget_id = Widget_id +func Capitalize(s string) string { + return New(s).Capitalize().String() +} + +// Capitalize will cap the first letter of string +// +// user = User +// bob dylan = Bob dylan +// widget_id = Widget_id +func (i Ident) Capitalize() Ident { + if len(i.Parts) == 0 { + return Ident{} + } + runes := []rune(i.Original) + runes[0] = unicode.ToTitle(runes[0]) + return Ident{Original: string(runes)} +} diff --git a/internal/core/core.go b/internal/core/core.go new file mode 100644 index 0000000..b259d29 --- /dev/null +++ b/internal/core/core.go @@ -0,0 +1,34 @@ +package core + +import ( + "strings" + "unicode" +) + +func isSpace(c rune) bool { + switch c { + case '_', ' ', ':', '-', '/': + return true + } + return unicode.IsSpace(c) +} + +func xappend(a []string, ss ...string) []string { + for _, s := range ss { + s = strings.TrimFunc(s, isSpace) + if up := strings.ToUpper(s); BaseAcronyms[up] { + s = up + } + if s != "" { + a = append(a, s) + } + } + return a +} + +func abs(x int) int { + if x < 0 { + return -x + } + return x +} diff --git a/internal/core/custom_data.go b/internal/core/custom_data.go new file mode 100644 index 0000000..7594cd0 --- /dev/null +++ b/internal/core/custom_data.go @@ -0,0 +1,87 @@ +package core + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +func init() { + loadCustomData("inflections.json", "INFLECT_PATH", "could not read inflection file", LoadInflections) + loadCustomData("acronyms.json", "ACRONYMS_PATH", "could not read acronyms file", LoadAcronyms) +} + +// CustomDataParser are functions that parse data like acronyms or +// plurals in the shape of a io.Reader it receives. +type CustomDataParser func(io.Reader) error + +func loadCustomData(defaultFile, env, readErrorMessage string, parser CustomDataParser) { + pwd, _ := os.Getwd() + path, found := os.LookupEnv(env) + if !found { + path = filepath.Join(pwd, defaultFile) + } + + if _, err := os.Stat(path); err != nil { + return + } + + b, err := os.ReadFile(path) + if err != nil { + fmt.Printf("%s %s (%s)\n", readErrorMessage, path, err) + return + } + + if err = parser(bytes.NewReader(b)); err != nil { + fmt.Println(err) + } +} + +// LoadAcronyms loads acronyms from io.Reader param +func LoadAcronyms(r io.Reader) error { + m := []string{} + err := json.NewDecoder(r).Decode(&m) + + if err != nil { + return fmt.Errorf("could not decode acronyms JSON from reader: %s", err) + } + + acronymsMoot.Lock() + defer acronymsMoot.Unlock() + + for _, acronym := range m { + BaseAcronyms[acronym] = true + } + + return nil +} + +// LoadInflections loads rules from io.Reader param +func LoadInflections(r io.Reader) error { + m := map[string]string{} + + err := json.NewDecoder(r).Decode(&m) + if err != nil { + return fmt.Errorf("could not decode inflection JSON from reader: %s", err) + } + + pluralMoot.Lock() + defer pluralMoot.Unlock() + singularMoot.Lock() + defer singularMoot.Unlock() + + for s, p := range m { + if strings.Contains(s, " ") || strings.Contains(p, " ") { + // flect works with parts, so multi-words should not be allowed + return fmt.Errorf("inflection elements should be a single word") + } + singleToPlural[s] = p + pluralToSingle[p] = s + } + + return nil +} diff --git a/internal/core/dasherize.go b/internal/core/dasherize.go new file mode 100644 index 0000000..4b42cfc --- /dev/null +++ b/internal/core/dasherize.go @@ -0,0 +1,36 @@ +package core + +import ( + "strings" + "unicode" +) + +// Dasherize returns an alphanumeric, lowercased, dashed string +// +// Donald E. Knuth = donald-e-knuth +// Test with + sign = test-with-sign +// admin/WidgetID = admin-widget-id +func Dasherize(s string) string { + return New(s).Dasherize().String() +} + +// Dasherize returns an alphanumeric, lowercased, dashed string +// +// Donald E. Knuth = donald-e-knuth +// Test with + sign = test-with-sign +// admin/WidgetID = admin-widget-id +func (i Ident) Dasherize() Ident { + var parts []string + + for _, part := range i.Parts { + var x strings.Builder + for _, c := range part { + if unicode.IsLetter(c) || unicode.IsDigit(c) { + x.WriteRune(c) + } + } + parts = xappend(parts, x.String()) + } + + return Ident{Original: strings.ToLower(strings.Join(parts, "-"))} +} diff --git a/internal/core/humanize.go b/internal/core/humanize.go new file mode 100644 index 0000000..0f7b07b --- /dev/null +++ b/internal/core/humanize.go @@ -0,0 +1,36 @@ +package core + +import ( + "strings" +) + +// Humanize returns first letter of sentence capitalized. +// Common acronyms are capitalized as well. +// Other capital letters in string are left as provided. +// +// employee_salary = Employee salary +// employee_id = employee ID +// employee_mobile_number = Employee mobile number +// first_Name = First Name +// firstName = First Name +func Humanize(s string) string { + return New(s).Humanize().String() +} + +// Humanize First letter of sentence capitalized +func (i Ident) Humanize() Ident { + if len(i.Original) == 0 { + return Ident{} + } + + if strings.TrimSpace(i.Original) == "" { + return i + } + + parts := xappend([]string{}, Titleize(i.Parts[0])) + if len(i.Parts) > 1 { + parts = xappend(parts, i.Parts[1:]...) + } + + return Ident{Original: strings.Join(parts, " ")} +} diff --git a/internal/core/ident.go b/internal/core/ident.go new file mode 100644 index 0000000..c5cd344 --- /dev/null +++ b/internal/core/ident.go @@ -0,0 +1,110 @@ +package core + +import ( + "encoding" + "strings" + "unicode" + "unicode/utf8" +) + +// Ident represents the string and it's parts +type Ident struct { + Original string + Parts []string +} + +// String implements fmt.Stringer and returns the original string +func (i Ident) String() string { + return i.Original +} + +// New creates a new Ident from the string +func New(s string) Ident { + i := Ident{ + Original: s, + Parts: toParts(s), + } + + return i +} + +func toParts(s string) []string { + parts := []string{} + s = strings.TrimSpace(s) + if len(s) == 0 { + return parts + } + if up := strings.ToUpper(s); BaseAcronyms[up] { + return []string{up} + } + var prev rune + var x strings.Builder + x.Grow(len(s)) + for _, c := range s { + if !utf8.ValidRune(c) { + continue + } + + if isSpace(c) { + parts = xappend(parts, x.String()) + x.Reset() + x.WriteRune(c) + prev = c + continue + } + + if unicode.IsUpper(c) && !unicode.IsUpper(prev) { + parts = xappend(parts, x.String()) + x.Reset() + x.WriteRune(c) + prev = c + continue + } + if unicode.IsUpper(c) && BaseAcronyms[x.String()] { + parts = xappend(parts, x.String()) + x.Reset() + x.WriteRune(c) + prev = c + continue + } + if unicode.IsLetter(c) || unicode.IsDigit(c) || unicode.IsPunct(c) || c == '`' { + prev = c + x.WriteRune(c) + continue + } + + parts = xappend(parts, x.String()) + x.Reset() + prev = c + } + parts = xappend(parts, x.String()) + + return parts +} + +var _ encoding.TextUnmarshaler = &Ident{} +var _ encoding.TextMarshaler = &Ident{} + +// LastPart returns the last part/word of the original string +func (i *Ident) LastPart() string { + if len(i.Parts) == 0 { + return "" + } + return i.Parts[len(i.Parts)-1] +} + +// ReplaceSuffix creates a new Ident with the original suffix replaced by new +func (i Ident) ReplaceSuffix(orig, new string) Ident { + return New(strings.TrimSuffix(i.Original, orig) + new) +} + +// UnmarshalText unmarshalls byte array into the Ident +func (i *Ident) UnmarshalText(data []byte) error { + (*i) = New(string(data)) + return nil +} + +// MarshalText marshals Ident into byte array +func (i Ident) MarshalText() ([]byte, error) { + return []byte(i.Original), nil +} diff --git a/internal/core/lower_upper.go b/internal/core/lower_upper.go new file mode 100644 index 0000000..bf448c7 --- /dev/null +++ b/internal/core/lower_upper.go @@ -0,0 +1,13 @@ +package core + +import "strings" + +// ToUpper is a convenience wrapper for strings.ToUpper +func (i Ident) ToUpper() Ident { + return Ident{Original: strings.ToUpper(i.Original)} +} + +// ToLower is a convenience wrapper for strings.ToLower +func (i Ident) ToLower() Ident { + return Ident{Original: strings.ToLower(i.Original)} +} diff --git a/internal/core/ordinalize.go b/internal/core/ordinalize.go new file mode 100644 index 0000000..f4030eb --- /dev/null +++ b/internal/core/ordinalize.go @@ -0,0 +1,41 @@ +package core + +import "strconv" + +// Ordinalize converts a number to an ordinal version +// +// 42 = 42nd +// 45 = 45th +// 1 = 1st +func Ordinalize(s string) string { + return New(s).Ordinalize().String() +} + +// Ordinalize converts a number to an ordinal version +// +// 42 = 42nd +// 45 = 45th +// 1 = 1st +func (i Ident) Ordinalize() Ident { + number, err := strconv.Atoi(i.Original) + if err != nil { + return i + } + var suffix string + switch abs(number) % 100 { + case 11, 12, 13: + suffix = "th" + default: + switch abs(number) % 10 { + case 1: + suffix = "st" + case 2: + suffix = "nd" + case 3: + suffix = "rd" + default: + suffix = "th" + } + } + return Ident{Original: strconv.Itoa(number) + suffix} +} diff --git a/internal/core/pascalize.go b/internal/core/pascalize.go new file mode 100644 index 0000000..061ec91 --- /dev/null +++ b/internal/core/pascalize.go @@ -0,0 +1,34 @@ +package core + +import ( + "strings" +) + +// Pascalize returns a string with each segment capitalized +// +// user = User +// bob dylan = BobDylan +// widget_id = WidgetID +func Pascalize(s string) string { + return New(s).Pascalize().String() +} + +// Pascalize returns a string with each segment capitalized +// +// user = User +// bob dylan = BobDylan +// widget_id = WidgetID +func (i Ident) Pascalize() Ident { + c := i.Camelize() + if len(c.String()) == 0 { + return c + } + if len(i.Parts) == 0 { + return i + } + capLen := 1 + if _, ok := BaseAcronyms[strings.ToUpper(i.Parts[0])]; ok { + capLen = len(i.Parts[0]) + } + return Ident{Original: string(strings.ToUpper(c.Original[0:capLen])) + c.Original[capLen:]} +} diff --git a/internal/core/plural_rules.go b/internal/core/plural_rules.go new file mode 100644 index 0000000..327daed --- /dev/null +++ b/internal/core/plural_rules.go @@ -0,0 +1,430 @@ +package core + +import "fmt" + +var pluralRules = []rule{} + +// AddPlural adds a rule that will replace the given suffix with the replacement suffix. +// The name is confusing. This function will be deprecated in the next release. +func AddPlural(suffix string, repl string) { + InsertPluralRule(suffix, repl) +} + +// InsertPluralRule inserts a rule that will replace the given suffix with +// the repl(acement) at the begining of the list of the pluralize rules. +func InsertPluralRule(suffix, repl string) { + pluralMoot.Lock() + defer pluralMoot.Unlock() + + pluralRules = append([]rule{{ + suffix: suffix, + fn: simpleRuleFunc(suffix, repl), + }}, pluralRules...) + + pluralRules = append([]rule{{ + suffix: repl, + fn: noop, + }}, pluralRules...) +} + +type Word struct { + Singular string + Plural string + Alternative string + Unidirectional bool // plural to singular is not possible (or bad) + Uncountable bool + Exact bool +} + +// dictionary is the main table for singularize and pluralize. +// All words in the dictionary will be added to singleToPlural, pluralToSingle +// and singlePluralAssertions by init() functions. +var Dictionary = []Word{ + // identicals https://en.wikipedia.org/wiki/English_plurals#Nouns_with_identical_singular_and_plural + {Singular: "aircraft", Plural: "aircraft"}, + {Singular: "beef", Plural: "beef", Alternative: "beefs"}, + {Singular: "bison", Plural: "bison"}, + {Singular: "blues", Plural: "blues", Unidirectional: true}, + {Singular: "chassis", Plural: "chassis"}, + {Singular: "deer", Plural: "deer"}, + {Singular: "fish", Plural: "fish", Alternative: "fishes"}, + {Singular: "moose", Plural: "moose"}, + {Singular: "police", Plural: "police"}, + {Singular: "salmon", Plural: "salmon", Alternative: "salmons"}, + {Singular: "series", Plural: "series"}, + {Singular: "sheep", Plural: "sheep"}, + {Singular: "shrimp", Plural: "shrimp", Alternative: "shrimps"}, + {Singular: "species", Plural: "species"}, + {Singular: "swine", Plural: "swine", Alternative: "swines"}, + {Singular: "trout", Plural: "trout", Alternative: "trouts"}, + {Singular: "tuna", Plural: "tuna", Alternative: "tunas"}, + {Singular: "you", Plural: "you"}, + // -en https://en.wikipedia.org/wiki/English_plurals#Plurals_in_-(e)n + {Singular: "child", Plural: "children"}, + {Singular: "ox", Plural: "oxen", Exact: true}, + // apophonic https://en.wikipedia.org/wiki/English_plurals#Apophonic_plurals + {Singular: "foot", Plural: "feet"}, + {Singular: "goose", Plural: "geese"}, + {Singular: "man", Plural: "men"}, + {Singular: "human", Plural: "humans"}, // not humen + {Singular: "louse", Plural: "lice", Exact: true}, + {Singular: "mouse", Plural: "mice"}, + {Singular: "tooth", Plural: "teeth"}, + {Singular: "woman", Plural: "women"}, + // misc https://en.wikipedia.org/wiki/English_plurals#Miscellaneous_irregular_plurals + {Singular: "die", Plural: "dice", Exact: true}, + {Singular: "person", Plural: "people"}, + + // Words from French that end in -u add an x; in addition to eau to eaux rule + {Singular: "adieu", Plural: "adieux", Alternative: "adieus"}, + {Singular: "fabliau", Plural: "fabliaux"}, + {Singular: "bureau", Plural: "bureaus", Alternative: "bureaux"}, // popular + + // Words from Greek that end in -on change -on to -a; in addition to hedron rule + {Singular: "criterion", Plural: "criteria"}, + {Singular: "ganglion", Plural: "ganglia", Alternative: "ganglions"}, + {Singular: "lexicon", Plural: "lexica", Alternative: "lexicons"}, + {Singular: "mitochondrion", Plural: "mitochondria", Alternative: "mitochondrions"}, + {Singular: "noumenon", Plural: "noumena"}, + {Singular: "phenomenon", Plural: "phenomena"}, + {Singular: "taxon", Plural: "taxa"}, + + // Words from Latin that end in -um change -um to -a; in addition to some rules + {Singular: "media", Plural: "media"}, // popular case: media -> media + {Singular: "medium", Plural: "media", Alternative: "mediums", Unidirectional: true}, + {Singular: "stadium", Plural: "stadiums", Alternative: "stadia"}, + {Singular: "aquarium", Plural: "aquaria", Alternative: "aquariums"}, + {Singular: "auditorium", Plural: "auditoria", Alternative: "auditoriums"}, + {Singular: "symposium", Plural: "symposia", Alternative: "symposiums"}, + {Singular: "curriculum", Plural: "curriculums", Alternative: "curricula"}, // ulum + {Singular: "quota", Plural: "quotas"}, + + // Words from Latin that end in -us change -us to -i or -era + {Singular: "alumnus", Plural: "alumni", Alternative: "alumnuses"}, // -i + {Singular: "bacillus", Plural: "bacilli"}, + {Singular: "cactus", Plural: "cacti", Alternative: "cactuses"}, + {Singular: "coccus", Plural: "cocci"}, + {Singular: "focus", Plural: "foci", Alternative: "focuses"}, + {Singular: "locus", Plural: "loci", Alternative: "locuses"}, + {Singular: "nucleus", Plural: "nuclei", Alternative: "nucleuses"}, + {Singular: "octopus", Plural: "octupuses", Alternative: "octopi"}, + {Singular: "radius", Plural: "radii", Alternative: "radiuses"}, + {Singular: "syllabus", Plural: "syllabi"}, + {Singular: "corpus", Plural: "corpora", Alternative: "corpuses"}, // -ra + {Singular: "genus", Plural: "genera"}, + + // Words from Latin that end in -a change -a to -ae + {Singular: "alumna", Plural: "alumnae"}, + {Singular: "vertebra", Plural: "vertebrae"}, + {Singular: "differentia", Plural: "differentiae"}, // -tia + {Singular: "minutia", Plural: "minutiae"}, + {Singular: "vita", Plural: "vitae"}, // -ita + {Singular: "larva", Plural: "larvae"}, // -va + {Singular: "postcava", Plural: "postcavae"}, + {Singular: "praecava", Plural: "praecavae"}, + {Singular: "uva", Plural: "uvae"}, + + // Words from Latin that end in -ex change -ex to -ices + {Singular: "apex", Plural: "apices", Alternative: "apexes"}, + {Singular: "codex", Plural: "codices", Alternative: "codexes"}, + {Singular: "index", Plural: "indices", Alternative: "indexes"}, + {Singular: "latex", Plural: "latices", Alternative: "latexes"}, + {Singular: "vertex", Plural: "vertices", Alternative: "vertexes"}, + {Singular: "vortex", Plural: "vortices", Alternative: "vortexes"}, + + // Words from Latin that end in -ix change -ix to -ices (eg, matrix becomes matrices) + {Singular: "appendix", Plural: "appendices", Alternative: "appendixes"}, + {Singular: "radix", Plural: "radices", Alternative: "radixes"}, + {Singular: "helix", Plural: "helices", Alternative: "helixes"}, + + // Words from Latin that end in -is change -is to -es + {Singular: "axis", Plural: "axes", Exact: true}, + {Singular: "crisis", Plural: "crises"}, + {Singular: "ellipsis", Plural: "ellipses", Unidirectional: true}, // ellipse + {Singular: "genesis", Plural: "geneses"}, + {Singular: "oasis", Plural: "oases"}, + {Singular: "thesis", Plural: "theses"}, + {Singular: "testis", Plural: "testes"}, + {Singular: "base", Plural: "bases"}, // popular case + {Singular: "basis", Plural: "bases", Unidirectional: true}, + + {Singular: "alias", Plural: "aliases", Exact: true}, // no alia, no aliasis + {Singular: "vedalia", Plural: "vedalias"}, // no vedalium, no vedaliases + + // Words that end in -ch, -o, -s, -sh, -x, -z (can be conflict with the others) + {Singular: "use", Plural: "uses", Exact: true}, // us vs use + {Singular: "abuse", Plural: "abuses"}, + {Singular: "cause", Plural: "causes"}, + {Singular: "clause", Plural: "clauses"}, + {Singular: "cruse", Plural: "cruses"}, + {Singular: "excuse", Plural: "excuses"}, + {Singular: "fuse", Plural: "fuses"}, + {Singular: "house", Plural: "houses"}, + {Singular: "misuse", Plural: "misuses"}, + {Singular: "muse", Plural: "muses"}, + {Singular: "pause", Plural: "pauses"}, + {Singular: "ache", Plural: "aches"}, + {Singular: "topaz", Plural: "topazes"}, + {Singular: "buffalo", Plural: "buffaloes", Alternative: "buffalos"}, + {Singular: "potato", Plural: "potatoes"}, + {Singular: "tomato", Plural: "tomatoes"}, + + // uncountables + {Singular: "equipment", Uncountable: true}, + {Singular: "information", Uncountable: true}, + {Singular: "jeans", Uncountable: true}, + {Singular: "money", Uncountable: true}, + {Singular: "news", Uncountable: true}, + {Singular: "rice", Uncountable: true}, + + // exceptions: -f to -ves, not -fe + {Singular: "dwarf", Plural: "dwarfs", Alternative: "dwarves"}, + {Singular: "hoof", Plural: "hoofs", Alternative: "hooves"}, + {Singular: "thief", Plural: "thieves"}, + // exceptions: instead of -f(e) to -ves + {Singular: "chive", Plural: "chives"}, + {Singular: "hive", Plural: "hives"}, + {Singular: "move", Plural: "moves"}, + + // exceptions: instead of -y to -ies + {Singular: "movie", Plural: "movies"}, + {Singular: "cookie", Plural: "cookies"}, + + // exceptions: instead of -um to -a + {Singular: "pretorium", Plural: "pretoriums"}, + {Singular: "agenda", Plural: "agendas"}, // instead of plural of agendum + // exceptions: instead of -um to -a (chemical element names) + + // Words from Latin that end in -a change -a to -ae + {Singular: "formula", Plural: "formulas", Alternative: "formulae"}, // also -um/-a + + // exceptions: instead of -o to -oes + {Singular: "shoe", Plural: "shoes"}, + {Singular: "toe", Plural: "toes", Exact: true}, + {Singular: "graffiti", Plural: "graffiti"}, + + // abbreviations + {Singular: "ID", Plural: "IDs", Exact: true}, +} + +// singleToPlural is the highest priority map for Pluralize(). +// singularToPluralSuffixList is used to build pluralRules for suffixes and +// compound words. +var singleToPlural = map[string]string{} + +// pluralToSingle is the highest priority map for Singularize(). +// singularToPluralSuffixList is used to build singularRules for suffixes and +// compound words. +var pluralToSingle = map[string]string{} + +// NOTE: This map should not be built as reverse map of singleToPlural since +// there are words that has the same plurals. + +// build singleToPlural and pluralToSingle with dictionary +func init() { + for _, wd := range Dictionary { + if singleToPlural[wd.Singular] != "" { + panic(fmt.Errorf("map singleToPlural already has an entry for %s", wd.Singular)) + } + + if wd.Uncountable && wd.Plural == "" { + wd.Plural = wd.Singular + } + + if wd.Plural == "" { + panic(fmt.Errorf("plural for %s is not provided", wd.Singular)) + } + + singleToPlural[wd.Singular] = wd.Plural + + if !wd.Unidirectional { + if pluralToSingle[wd.Plural] != "" { + panic(fmt.Errorf("map pluralToSingle already has an entry for %s", wd.Plural)) + } + pluralToSingle[wd.Plural] = wd.Singular + + if wd.Alternative != "" { + if pluralToSingle[wd.Alternative] != "" { + panic(fmt.Errorf("map pluralToSingle already has an entry for %s", wd.Alternative)) + } + pluralToSingle[wd.Alternative] = wd.Singular + } + } + } +} + +type singularToPluralSuffix struct { + singular string + plural string +} + +// singularToPluralSuffixList is a list of "bidirectional" suffix rules for +// the irregular plurals follow such rules. +// +// NOTE: IMPORTANT! The order of items in this list is the rule priority, not +// alphabet order. The first match will be used to inflect. +var singularToPluralSuffixList = []singularToPluralSuffix{ + // https://en.wiktionary.org/wiki/Appendix:English_irregular_nouns#Rules + // Words that end in -f or -fe change -f or -fe to -ves + {"tive", "tives"}, // exception + {"eaf", "eaves"}, + {"oaf", "oaves"}, + {"afe", "aves"}, + {"arf", "arves"}, + {"rfe", "rves"}, + {"rf", "rves"}, + {"lf", "lves"}, + {"fe", "ves"}, // previously '[a-eg-km-z]fe' TODO: regex support + + // Words that end in -y preceded by a consonant change -y to -ies + {"ay", "ays"}, + {"ey", "eys"}, + {"oy", "oys"}, + {"quy", "quies"}, + {"uy", "uys"}, + {"y", "ies"}, // '[^aeiou]y' + + // Words from French that end in -u add an x (eg, château becomes châteaux) + {"eau", "eaux"}, // it seems like 'eau' is the most popular form of this rule + + // Words from Latin that end in -a change -a to -ae; before -on to -a and -um to -a + {"bula", "bulae"}, + {"dula", "bulae"}, + {"lula", "bulae"}, + {"nula", "bulae"}, + {"vula", "bulae"}, + + // Words from Greek that end in -on change -on to -a (eg, polyhedron becomes polyhedra) + // https://en.wiktionary.org/wiki/Category:English_irregular_plurals_ending_in_"-a" + {"hedron", "hedra"}, + + // Words from Latin that end in -um change -um to -a (eg, minimum becomes minima) + // https://en.wiktionary.org/wiki/Category:English_irregular_plurals_ending_in_"-a" + {"ium", "ia"}, // some exceptions especially chemical element names + {"seum", "seums"}, + {"eum", "ea"}, + {"oum", "oa"}, + {"stracum", "straca"}, + {"dum", "da"}, + {"elum", "ela"}, + {"ilum", "ila"}, + {"olum", "ola"}, + {"ulum", "ula"}, + {"llum", "lla"}, + {"ylum", "yla"}, + {"imum", "ima"}, + {"ernum", "erna"}, + {"gnum", "gna"}, + {"brum", "bra"}, + {"crum", "cra"}, + {"terum", "tera"}, + {"serum", "sera"}, + {"trum", "tra"}, + {"antum", "anta"}, + {"atum", "ata"}, + {"entum", "enta"}, + {"etum", "eta"}, + {"itum", "ita"}, + {"otum", "ota"}, + {"utum", "uta"}, + {"ctum", "cta"}, + {"ovum", "ova"}, + + // Words from Latin that end in -us change -us to -i or -era + // not easy to make a simple rule. just add them all to the dictionary + + // Words from Latin that end in -ex change -ex to -ices (eg, vortex becomes vortices) + // Words from Latin that end in -ix change -ix to -ices (eg, matrix becomes matrices) + // for example, -dix, -dex, and -dice will have the same plural form so + // making a simple rule is not possible for them + {"trix", "trices"}, // ignore a few words end in trice + + // Words from Latin that end in -is change -is to -es (eg, thesis becomes theses) + // -sis and -se has the same plural -ses so making a rule is not easy too. + {"iasis", "iases"}, + {"mesis", "meses"}, + {"kinesis", "kineses"}, + {"resis", "reses"}, + {"gnosis", "gnoses"}, // e.g. diagnosis + {"opsis", "opses"}, // e.g. synopsis + {"ysis", "yses"}, // e.g. analysis + + // Words that end in -ch, -o, -s, -sh, -x, -z + {"ouse", "ouses"}, + {"lause", "lauses"}, + {"us", "uses"}, // use/uses is in the dictionary + + {"ch", "ches"}, + {"io", "ios"}, + {"sh", "shes"}, + {"ss", "sses"}, + {"ez", "ezzes"}, + {"iz", "izzes"}, + {"tz", "tzes"}, + {"zz", "zzes"}, + {"ano", "anos"}, + {"lo", "los"}, + {"to", "tos"}, + {"oo", "oos"}, + {"o", "oes"}, + {"x", "xes"}, + + // for abbreviations + {"S", "Ses"}, +} + +func init() { + nSuffix := len(singularToPluralSuffixList) + nDict := len(Dictionary) + + newPlural := make([]rule, 0, 2*(nDict+nSuffix)) + newSingular := make([]rule, 0, 3*(nDict+nSuffix)) // 3× to account for Alternative entries + + // Dict compound rules (higher priority). + // Original code prepended in forward order → last entry ended at index 0. + // Replicate with append by iterating backward so last entry is appended first. + for i := nDict - 1; i >= 0; i-- { + wd := Dictionary[i] + if wd.Exact { + continue + } + if wd.Uncountable && wd.Plural == "" { + wd.Plural = wd.Singular + } + newPlural = append(newPlural, + rule{suffix: wd.Plural, fn: noop}, + rule{suffix: wd.Singular, fn: simpleRuleFunc(wd.Singular, wd.Plural)}, + ) + if !wd.Unidirectional { + // Alternative was inserted last (highest priority among the two), + // so it must be appended first here. + if wd.Alternative != "" { + newSingular = append(newSingular, + rule{suffix: wd.Singular, fn: noop}, + rule{suffix: wd.Alternative, fn: simpleRuleFunc(wd.Alternative, wd.Singular)}, + ) + } + newSingular = append(newSingular, + rule{suffix: wd.Singular, fn: noop}, + rule{suffix: wd.Plural, fn: simpleRuleFunc(wd.Plural, wd.Singular)}, + ) + } + } + + // Suffix rules (lower priority). + // Original code iterated backward and prepended → element 0 ended at index 0. + // Replicate with append by iterating forward. + for _, s := range singularToPluralSuffixList { + newPlural = append(newPlural, + rule{suffix: s.plural, fn: noop}, + rule{suffix: s.singular, fn: simpleRuleFunc(s.singular, s.plural)}, + ) + newSingular = append(newSingular, + rule{suffix: s.singular, fn: noop}, + rule{suffix: s.plural, fn: simpleRuleFunc(s.plural, s.singular)}, + ) + } + + pluralRules = newPlural + singularRules = newSingular +} diff --git a/internal/core/pluralize.go b/internal/core/pluralize.go new file mode 100644 index 0000000..f478ccc --- /dev/null +++ b/internal/core/pluralize.go @@ -0,0 +1,81 @@ +package core + +import ( + "strings" + "sync" +) + +var pluralMoot = &sync.RWMutex{} + +// Pluralize returns a plural version of the string +// +// user = users +// person = people +// datum = data +func Pluralize(s string) string { + return New(s).Pluralize().String() +} + +// PluralizeWithSize will pluralize a string taking a number into account. +// +// PluralizeWithSize("user", 1) = user +// PluralizeWithSize("user", 2) = users +func PluralizeWithSize(s string, i int) string { + if i == 1 || i == -1 { + return New(s).Singularize().String() + } + return New(s).Pluralize().String() +} + +// Pluralize returns a plural version of the string +// +// user = users +// person = people +// datum = data +func (i Ident) Pluralize() Ident { + s := i.LastPart() + if len(s) == 0 { + return New("") + } + + pluralMoot.RLock() + + // check if the Original has an explicit entry in the map + if p, ok := singleToPlural[i.Original]; ok { + pluralMoot.RUnlock() + return i.ReplaceSuffix(i.Original, p) + } + if _, ok := pluralToSingle[i.Original]; ok { + pluralMoot.RUnlock() + return i + } + + ls := strings.ToLower(s) + if _, ok := pluralToSingle[ls]; ok { + pluralMoot.RUnlock() + return i + } + + if p, ok := singleToPlural[ls]; ok { + if s == Capitalize(s) { + p = Capitalize(p) + } + pluralMoot.RUnlock() + return i.ReplaceSuffix(s, p) + } + + rules := pluralRules + pluralMoot.RUnlock() + + for _, r := range rules { + if strings.HasSuffix(s, r.suffix) { + return i.ReplaceSuffix(s, r.fn(s)) + } + } + + if strings.HasSuffix(ls, "s") { + return i + } + + return New(i.String() + "s") +} diff --git a/internal/core/rule.go b/internal/core/rule.go new file mode 100644 index 0000000..df20398 --- /dev/null +++ b/internal/core/rule.go @@ -0,0 +1,17 @@ +package core + +type ruleFn func(string) string + +type rule struct { + suffix string + fn ruleFn +} + +func simpleRuleFunc(suffix, repl string) func(string) string { + return func(s string) string { + s = s[:len(s)-len(suffix)] + return s + repl + } +} + +func noop(s string) string { return s } diff --git a/internal/core/singular_rules.go b/internal/core/singular_rules.go new file mode 100644 index 0000000..ccb45de --- /dev/null +++ b/internal/core/singular_rules.go @@ -0,0 +1,26 @@ +package core + +var singularRules = []rule{} + +// AddSingular adds a rule that will replace the given suffix with the replacement suffix. +// The name is confusing. This function will be deprecated in the next release. +func AddSingular(ext string, repl string) { + InsertSingularRule(ext, repl) +} + +// InsertSingularRule inserts a rule that will replace the given suffix with +// the repl(acement) at the beginning of the list of the singularize rules. +func InsertSingularRule(suffix, repl string) { + singularMoot.Lock() + defer singularMoot.Unlock() + + singularRules = append([]rule{{ + suffix: suffix, + fn: simpleRuleFunc(suffix, repl), + }}, singularRules...) + + singularRules = append([]rule{{ + suffix: repl, + fn: noop, + }}, singularRules...) +} diff --git a/internal/core/singularize.go b/internal/core/singularize.go new file mode 100644 index 0000000..ca66c73 --- /dev/null +++ b/internal/core/singularize.go @@ -0,0 +1,78 @@ +package core + +import ( + "strings" + "sync" +) + +var singularMoot = &sync.RWMutex{} + +// Singularize returns a singular version of the string +// +// users = user +// data = datum +// people = person +func Singularize(s string) string { + return New(s).Singularize().String() +} + +// SingularizeWithSize will singularize a string taking a number into account. +// +// SingularizeWithSize("user", 1) = user +// SingularizeWithSize("user", 2) = users +func SingularizeWithSize(s string, i int) string { + return PluralizeWithSize(s, i) +} + +// Singularize returns a singular version of the string +// +// users = user +// data = datum +// people = person +func (i Ident) Singularize() Ident { + s := i.LastPart() + if len(s) == 0 { + return i + } + + singularMoot.RLock() + + // check if the Original has an explicit entry in the map + if p, ok := pluralToSingle[i.Original]; ok { + singularMoot.RUnlock() + return i.ReplaceSuffix(i.Original, p) + } + if _, ok := singleToPlural[i.Original]; ok { + singularMoot.RUnlock() + return i + } + + ls := strings.ToLower(s) + if p, ok := pluralToSingle[ls]; ok { + if s == Capitalize(s) { + p = Capitalize(p) + } + singularMoot.RUnlock() + return i.ReplaceSuffix(s, p) + } + + if _, ok := singleToPlural[ls]; ok { + singularMoot.RUnlock() + return i + } + + rules := singularRules + singularMoot.RUnlock() + + for _, r := range rules { + if strings.HasSuffix(s, r.suffix) { + return i.ReplaceSuffix(s, r.fn(s)) + } + } + + if strings.HasSuffix(s, "s") { + return i.ReplaceSuffix("s", "") + } + + return i +} diff --git a/internal/core/titleize.go b/internal/core/titleize.go new file mode 100644 index 0000000..a265480 --- /dev/null +++ b/internal/core/titleize.go @@ -0,0 +1,40 @@ +package core + +import ( + "strings" + "unicode" +) + +// Titleize will capitalize the start of each part +// +// "Nice to see you!" = "Nice To See You!" +// "i've read a book! have you?" = "I've Read A Book! Have You?" +// "This is `code` ok" = "This Is `code` OK" +func Titleize(s string) string { + return New(s).Titleize().String() +} + +// Titleize will capitalize the start of each part +// +// "Nice to see you!" = "Nice To See You!" +// "i've read a book! have you?" = "I've Read A Book! Have You?" +// "This is `code` ok" = "This Is `code` OK" +func (i Ident) Titleize() Ident { + var parts []string + + // TODO: we need to reconsider the design. + // this approach preserves inline code block as is but it also + // preserves the other words start with a special character. + // I would prefer: "*wonderful* world" to be "*Wonderful* World" + for _, part := range i.Parts { + // CAUTION: in unicode, []rune(str)[0] is not rune(str[0]) + runes := []rune(part) + x := string(unicode.ToTitle(runes[0])) + if len(runes) > 1 { + x += string(runes[1:]) + } + parts = append(parts, x) + } + + return Ident{Original: strings.Join(parts, " ")} +} diff --git a/internal/core/underscore.go b/internal/core/underscore.go new file mode 100644 index 0000000..e2f2992 --- /dev/null +++ b/internal/core/underscore.go @@ -0,0 +1,37 @@ +package core + +import ( + "strings" + "unicode" +) + +// Underscore a string +// +// bob dylan --> bob_dylan +// Nice to see you! --> nice_to_see_you +// widgetID --> widget_id +func Underscore(s string) string { + return New(s).Underscore().String() +} + +// Underscore a string +// +// bob dylan --> bob_dylan +// Nice to see you! --> nice_to_see_you +// widgetID --> widget_id +func (i Ident) Underscore() Ident { + out := make([]string, 0, len(i.Parts)) + for _, part := range i.Parts { + var x strings.Builder + x.Grow(len(part)) + for _, c := range part { + if unicode.IsLetter(c) || unicode.IsDigit(c) { + x.WriteRune(c) + } + } + if x.Len() > 0 { + out = append(out, x.String()) + } + } + return New(strings.ToLower(strings.Join(out, "_"))) +} diff --git a/internal/testhelpers/testhelpers.go b/internal/testhelpers/testhelpers.go new file mode 100644 index 0000000..a377d2a --- /dev/null +++ b/internal/testhelpers/testhelpers.go @@ -0,0 +1,59 @@ +package testhelpers + +import ( + "fmt" + "reflect" + "testing" +) + +type RequireHelper struct { + T testing.TB +} + +func NewRequire(t testing.TB) *RequireHelper { + t.Helper() + return &RequireHelper{T: t} +} + +func (r *RequireHelper) Equal(exp, got any, msgAndArgs ...any) { + r.T.Helper() + if !reflect.DeepEqual(exp, got) { + msg := FormatTestMsg(msgAndArgs) + if msg != "" { + r.T.Fatalf("%s\nexpected: %v\ngot: %v", msg, exp, got) + } else { + r.T.Fatalf("expected: %v\ngot: %v", exp, got) + } + } +} + +func (r *RequireHelper) NoError(err error) { + r.T.Helper() + if err != nil { + r.T.Fatalf("unexpected error: %v", err) + } +} + +func (r *RequireHelper) Error(err error) { + r.T.Helper() + if err == nil { + r.T.Fatal("expected error, got nil") + } +} + +func (r *RequireHelper) True(cond bool) { + r.T.Helper() + if !cond { + r.T.Fatal("expected true, got false") + } +} + +func FormatTestMsg(msgAndArgs []any) string { + if len(msgAndArgs) == 0 { + return "" + } + if msg, ok := msgAndArgs[0].(string); ok { + return fmt.Sprintf(msg, msgAndArgs[1:]...) + } + return fmt.Sprintf("%v", msgAndArgs[0]) +} diff --git a/lower_upper.go b/lower_upper.go index 930da58..ea7835f 100644 --- a/lower_upper.go +++ b/lower_upper.go @@ -1,13 +1 @@ package flect - -import "strings" - -// ToUpper is a convience wrapper for strings.ToUpper -func (i Ident) ToUpper() Ident { - return New(strings.ToUpper(i.Original)) -} - -// ToLower is a convience wrapper for strings.ToLower -func (i Ident) ToLower() Ident { - return New(strings.ToLower(i.Original)) -} diff --git a/name/char_test.go b/name/char_test.go index 36b82fd..25167b4 100644 --- a/name/char_test.go +++ b/name/char_test.go @@ -2,8 +2,6 @@ package name import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_Char(t *testing.T) { @@ -17,7 +15,7 @@ func Test_Char(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, Char(tt.act)) r.Equal(tt.exp, Char(tt.exp)) }) diff --git a/name/file.go b/name/file.go index ec5cdae..5eebdb2 100644 --- a/name/file.go +++ b/name/file.go @@ -3,10 +3,11 @@ package name import ( "strings" - "github.com/gobuffalo/flect" + core "github.com/gobuffalo/flect/internal/core" ) // File creates a suitable file name +// // admin/widget = admin/widget // foo_bar = foo_bar // U$ser = u_ser @@ -15,6 +16,7 @@ func File(s string, exts ...string) string { } // File creates a suitable file name +// // admin/widget = admin/widget // foo_bar = foo_bar // U$ser = u_ser @@ -22,7 +24,7 @@ func (i Ident) File(exts ...string) Ident { var parts []string for _, part := range strings.Split(i.Original, "/") { - parts = append(parts, flect.Underscore(part)) + parts = append(parts, core.Underscore(part)) } return New(strings.Join(parts, "/") + strings.Join(exts, "")) } diff --git a/name/file_test.go b/name/file_test.go index 44e7cf1..c048103 100644 --- a/name/file_test.go +++ b/name/file_test.go @@ -2,8 +2,6 @@ package name import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_File(t *testing.T) { @@ -20,7 +18,7 @@ func Test_File(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, File(tt.act)) r.Equal(tt.exp, File(tt.exp)) r.Equal(tt.exp+".a.b", File(tt.act, ".a", ".b")) diff --git a/name/folder.go b/name/folder.go index 5b7b978..7b95f8f 100644 --- a/name/folder.go +++ b/name/folder.go @@ -4,7 +4,7 @@ import ( "regexp" "strings" - "github.com/gobuffalo/flect" + core "github.com/gobuffalo/flect/internal/core" ) var alphanum = regexp.MustCompile(`[^a-zA-Z0-9_]+`) @@ -12,6 +12,7 @@ var alphanum = regexp.MustCompile(`[^a-zA-Z0-9_]+`) // Folder returns a suitable folder name. It removes any special characters // from the given string `s` and returns a string consists of alpha-numeric // characters. +// // admin/widget --> admin/widget // adminUser --> admin_user // foo_bar --> foo_bar @@ -24,6 +25,7 @@ func Folder(s string, exts ...string) string { // Folder returns a suitable folder name. It removes any special characters // from the given string `s` and returns a string consists of alpha-numeric // characters. +// // admin/widget --> admin/widget // adminUser --> admin_user // foo_bar --> foo_bar @@ -33,7 +35,7 @@ func (i Ident) Folder(exts ...string) Ident { var parts []string for _, part := range strings.Split(i.Original, "/") { - part = alphanum.ReplaceAllString(flect.Underscore(part), "") + part = alphanum.ReplaceAllString(core.Underscore(part), "") parts = append(parts, part) } diff --git a/name/folder_test.go b/name/folder_test.go index 42f6f00..56b9e09 100644 --- a/name/folder_test.go +++ b/name/folder_test.go @@ -2,8 +2,6 @@ package name import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_Folder(t *testing.T) { @@ -25,7 +23,7 @@ func Test_Folder(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, Folder(tt.act)) r.Equal(tt.exp, Folder(tt.exp)) r.Equal(tt.exp+".a.b", Folder(tt.act, ".a", ".b")) diff --git a/name/ident.go b/name/ident.go index 91f5d8b..1204ae4 100644 --- a/name/ident.go +++ b/name/ident.go @@ -1,13 +1,13 @@ package name -import "github.com/gobuffalo/flect" +import core "github.com/gobuffalo/flect/internal/core" // Ident represents the string and it's parts type Ident struct { - flect.Ident + core.Ident } // New creates a new Ident from the string func New(s string) Ident { - return Ident{flect.New(s)} + return Ident{core.New(s)} } diff --git a/name/interface.go b/name/interface.go index a3590c8..fe98fb9 100644 --- a/name/interface.go +++ b/name/interface.go @@ -5,7 +5,7 @@ import ( "reflect" ) -func Interface(x interface{}) (Ident, error) { +func Interface(x any) (Ident, error) { switch t := x.(type) { case string: return New(t), nil diff --git a/name/interface_test.go b/name/interface_test.go index 2ed21e4..70f8b8d 100644 --- a/name/interface_test.go +++ b/name/interface_test.go @@ -3,15 +3,13 @@ package name import ( "fmt" "testing" - - "github.com/stretchr/testify/require" ) type car struct{} func Test_Interface(t *testing.T) { table := []struct { - in interface{} + in any out string err bool }{ @@ -24,7 +22,7 @@ func Test_Interface(t *testing.T) { for _, tt := range table { t.Run(fmt.Sprint(tt.in), func(st *testing.T) { - r := require.New(st) + r := newRequire(st) n, err := Interface(tt.in) if tt.err { r.Error(err) diff --git a/name/join_test.go b/name/join_test.go index 48d58e2..e340911 100644 --- a/name/join_test.go +++ b/name/join_test.go @@ -3,8 +3,6 @@ package name import ( "runtime" "testing" - - "github.com/stretchr/testify/require" ) func Test_Ident_FilePathJoin(t *testing.T) { @@ -19,7 +17,7 @@ func Test_Ident_FilePathJoin(t *testing.T) { for in, out := range table { t.Run(in, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(out, FilePathJoin(in, "boo")) }) } diff --git a/name/key_test.go b/name/key_test.go index f478989..b537a4d 100644 --- a/name/key_test.go +++ b/name/key_test.go @@ -2,8 +2,6 @@ package name import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_Ident_Key(t *testing.T) { @@ -14,7 +12,7 @@ func Test_Ident_Key(t *testing.T) { for in, out := range table { t.Run(in, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(out, Key(in)) }) } diff --git a/name/name.go b/name/name.go index c90cfb2..ca910d5 100644 --- a/name/name.go +++ b/name/name.go @@ -4,10 +4,11 @@ import ( "encoding" "strings" - "github.com/gobuffalo/flect" + core "github.com/gobuffalo/flect/internal/core" ) // Proper pascalizes and singularizes the string +// // person = Person // foo_bar = FooBar // admin/widgets = AdminWidget @@ -16,6 +17,7 @@ func Proper(s string) string { } // Proper pascalizes and singularizes the string +// // person = Person // foo_bar = FooBar // admin/widgets = AdminWidget @@ -24,6 +26,7 @@ func (i Ident) Proper() Ident { } // Group pascalizes and pluralizes the string +// // person = People // foo_bar = FooBars // admin/widget = AdminWidgets @@ -32,6 +35,7 @@ func Group(s string) string { } // Group pascalizes and pluralizes the string +// // person = People // foo_bar = FooBars // admin/widget = AdminWidgets @@ -42,7 +46,7 @@ func (i Ident) Group() Ident { } last := i.Parts[len(i.Parts)-1] for _, part := range i.Parts[:len(i.Parts)-1] { - parts = append(parts, flect.Pascalize(part)) + parts = append(parts, core.Pascalize(part)) } last = New(last).Pluralize().Pascalize().String() parts = append(parts, last) diff --git a/name/name_test.go b/name/name_test.go index 42be425..4c0d142 100644 --- a/name/name_test.go +++ b/name/name_test.go @@ -2,8 +2,6 @@ package name import ( "testing" - - "github.com/stretchr/testify/require" ) type tt struct { @@ -38,7 +36,7 @@ func Test_Name(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, Proper(tt.act)) r.Equal(tt.exp, Proper(tt.exp)) }) @@ -63,7 +61,7 @@ func Test_Group(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, Group(tt.act)) r.Equal(tt.exp, Group(tt.exp)) }) @@ -71,7 +69,7 @@ func Test_Group(t *testing.T) { } func Test_MarshalText(t *testing.T) { - r := require.New(t) + r := newRequire(t) n := New("mark") b, err := n.MarshalText() diff --git a/name/os_path_test.go b/name/os_path_test.go index bf6e16b..0476e89 100644 --- a/name/os_path_test.go +++ b/name/os_path_test.go @@ -3,8 +3,6 @@ package name import ( "runtime" "testing" - - "github.com/stretchr/testify/require" ) func Test_Ident_OsPath(t *testing.T) { @@ -19,7 +17,7 @@ func Test_Ident_OsPath(t *testing.T) { for in, out := range table { t.Run(in, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(out, OsPath(in)) }) } diff --git a/name/package_test.go b/name/package_test.go index d363f85..95cfe41 100644 --- a/name/package_test.go +++ b/name/package_test.go @@ -4,8 +4,6 @@ import ( "go/build" "path/filepath" "testing" - - "github.com/stretchr/testify/require" ) func Test_Package(t *testing.T) { @@ -31,7 +29,7 @@ func Test_Package(t *testing.T) { } for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, Package(tt.act)) r.Equal(tt.exp, Package(tt.exp)) }) diff --git a/name/param_id_test.go b/name/param_id_test.go index 346d034..a5a1a63 100644 --- a/name/param_id_test.go +++ b/name/param_id_test.go @@ -2,8 +2,6 @@ package name import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_ParamID(t *testing.T) { @@ -19,7 +17,7 @@ func Test_ParamID(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, ParamID(tt.act)) r.Equal(tt.exp, ParamID(tt.exp)) }) diff --git a/name/resource_test.go b/name/resource_test.go index 59d9328..337ef39 100644 --- a/name/resource_test.go +++ b/name/resource_test.go @@ -2,12 +2,10 @@ package name import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_Name_Resource(t *testing.T) { - r := require.New(t) + r := newRequire(t) table := []struct { V string E string diff --git a/name/tablize_test.go b/name/tablize_test.go index 91ad1a4..3733fbb 100644 --- a/name/tablize_test.go +++ b/name/tablize_test.go @@ -2,8 +2,6 @@ package name import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_Tableize(t *testing.T) { @@ -29,7 +27,7 @@ func Test_Tableize(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, Tableize(tt.act)) r.Equal(tt.exp, Tableize(tt.exp)) }) diff --git a/name/testhelpers_test.go b/name/testhelpers_test.go new file mode 100644 index 0000000..c519e85 --- /dev/null +++ b/name/testhelpers_test.go @@ -0,0 +1,13 @@ +package name + +import ( + "testing" + + th "github.com/gobuffalo/flect/internal/testhelpers" +) + +type requireHelper = th.RequireHelper + +func newRequire(t testing.TB) *requireHelper { + return th.NewRequire(t) +} diff --git a/name/url_test.go b/name/url_test.go index c5b79e5..32c944c 100644 --- a/name/url_test.go +++ b/name/url_test.go @@ -2,8 +2,6 @@ package name import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_URL(t *testing.T) { @@ -21,7 +19,7 @@ func Test_URL(t *testing.T) { for _, tt := range table { t.Run(tt.in, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) n := New(tt.in) r.Equal(tt.out, n.URL().String(), "URL of %v", tt.in) }) diff --git a/name/var_case_test.go b/name/var_case_test.go index a8f9734..16023f5 100644 --- a/name/var_case_test.go +++ b/name/var_case_test.go @@ -2,8 +2,6 @@ package name import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_VarCaseSingle(t *testing.T) { @@ -22,7 +20,7 @@ func Test_VarCaseSingle(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, VarCaseSingle(tt.act)) r.Equal(tt.exp, VarCaseSingle(tt.exp)) }) @@ -45,7 +43,7 @@ func Test_VarCasePlural(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, VarCasePlural(tt.act)) r.Equal(tt.exp, VarCasePlural(tt.exp)) }) @@ -69,7 +67,7 @@ func Test_VarCase(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, VarCase(tt.act)) r.Equal(tt.exp, VarCase(tt.exp)) }) diff --git a/ordinalize.go b/ordinalize.go index 1ce27b3..dfc0a9a 100644 --- a/ordinalize.go +++ b/ordinalize.go @@ -1,43 +1,12 @@ package flect -import ( - "fmt" - "strconv" -) +import "github.com/gobuffalo/flect/internal/core" // Ordinalize converts a number to an ordinal version +// // 42 = 42nd // 45 = 45th // 1 = 1st func Ordinalize(s string) string { - return New(s).Ordinalize().String() -} - -// Ordinalize converts a number to an ordinal version -// 42 = 42nd -// 45 = 45th -// 1 = 1st -func (i Ident) Ordinalize() Ident { - number, err := strconv.Atoi(i.Original) - if err != nil { - return i - } - var s string - switch abs(number) % 100 { - case 11, 12, 13: - s = fmt.Sprintf("%dth", number) - default: - switch abs(number) % 10 { - case 1: - s = fmt.Sprintf("%dst", number) - case 2: - s = fmt.Sprintf("%dnd", number) - case 3: - s = fmt.Sprintf("%drd", number) - } - } - if s != "" { - return New(s) - } - return New(fmt.Sprintf("%dth", number)) + return core.Ordinalize(s) } diff --git a/ordinalize_test.go b/ordinalize_test.go index 0c9fadc..3b77e89 100644 --- a/ordinalize_test.go +++ b/ordinalize_test.go @@ -2,8 +2,6 @@ package flect import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_Ordinalize(t *testing.T) { @@ -73,7 +71,7 @@ func Test_Ordinalize(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, Ordinalize(tt.act)) r.Equal(tt.exp, Ordinalize(tt.exp)) }) diff --git a/pascalize.go b/pascalize.go index 6396d0d..c5b2c86 100644 --- a/pascalize.go +++ b/pascalize.go @@ -1,32 +1,12 @@ package flect -import ( - "strings" -) +import "github.com/gobuffalo/flect/internal/core" // Pascalize returns a string with each segment capitalized +// // user = User // bob dylan = BobDylan // widget_id = WidgetID func Pascalize(s string) string { - return New(s).Pascalize().String() -} - -// Pascalize returns a string with each segment capitalized -// user = User -// bob dylan = BobDylan -// widget_id = WidgetID -func (i Ident) Pascalize() Ident { - c := i.Camelize() - if len(c.String()) == 0 { - return c - } - if len(i.Parts) == 0 { - return i - } - capLen := 1 - if _, ok := baseAcronyms[strings.ToUpper(i.Parts[0])]; ok { - capLen = len(i.Parts[0]) - } - return New(string(strings.ToUpper(c.Original[0:capLen])) + c.Original[capLen:]) + return core.Pascalize(s) } diff --git a/pascalize_test.go b/pascalize_test.go index 9a3a5b7..29f6d41 100644 --- a/pascalize_test.go +++ b/pascalize_test.go @@ -2,8 +2,6 @@ package flect import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_Pascalize(t *testing.T) { @@ -26,7 +24,7 @@ func Test_Pascalize(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, Pascalize(tt.act)) r.Equal(tt.exp, Pascalize(tt.exp)) }) diff --git a/plural_rules.go b/plural_rules.go index 3904e79..7044c14 100644 --- a/plural_rules.go +++ b/plural_rules.go @@ -1,417 +1,15 @@ package flect -import "fmt" - -var pluralRules = []rule{} +import "github.com/gobuffalo/flect/internal/core" // AddPlural adds a rule that will replace the given suffix with the replacement suffix. // The name is confusing. This function will be deprecated in the next release. func AddPlural(suffix string, repl string) { - InsertPluralRule(suffix, repl) + core.AddPlural(suffix, repl) } // InsertPluralRule inserts a rule that will replace the given suffix with // the repl(acement) at the begining of the list of the pluralize rules. func InsertPluralRule(suffix, repl string) { - pluralMoot.Lock() - defer pluralMoot.Unlock() - - pluralRules = append([]rule{{ - suffix: suffix, - fn: simpleRuleFunc(suffix, repl), - }}, pluralRules...) - - pluralRules = append([]rule{{ - suffix: repl, - fn: noop, - }}, pluralRules...) -} - -type word struct { - singular string - plural string - alternative string - unidirectional bool // plural to singular is not possible (or bad) - uncountable bool - exact bool -} - -// dictionary is the main table for singularize and pluralize. -// All words in the dictionary will be added to singleToPlural, pluralToSingle -// and singlePluralAssertions by init() functions. -var dictionary = []word{ - // identicals https://en.wikipedia.org/wiki/English_plurals#Nouns_with_identical_singular_and_plural - {singular: "aircraft", plural: "aircraft"}, - {singular: "beef", plural: "beef", alternative: "beefs"}, - {singular: "bison", plural: "bison"}, - {singular: "blues", plural: "blues", unidirectional: true}, - {singular: "chassis", plural: "chassis"}, - {singular: "deer", plural: "deer"}, - {singular: "fish", plural: "fish", alternative: "fishes"}, - {singular: "moose", plural: "moose"}, - {singular: "police", plural: "police"}, - {singular: "salmon", plural: "salmon", alternative: "salmons"}, - {singular: "series", plural: "series"}, - {singular: "sheep", plural: "sheep"}, - {singular: "shrimp", plural: "shrimp", alternative: "shrimps"}, - {singular: "species", plural: "species"}, - {singular: "swine", plural: "swine", alternative: "swines"}, - {singular: "trout", plural: "trout", alternative: "trouts"}, - {singular: "tuna", plural: "tuna", alternative: "tunas"}, - {singular: "you", plural: "you"}, - // -en https://en.wikipedia.org/wiki/English_plurals#Plurals_in_-(e)n - {singular: "child", plural: "children"}, - {singular: "ox", plural: "oxen", exact: true}, - // apophonic https://en.wikipedia.org/wiki/English_plurals#Apophonic_plurals - {singular: "foot", plural: "feet"}, - {singular: "goose", plural: "geese"}, - {singular: "man", plural: "men"}, - {singular: "human", plural: "humans"}, // not humen - {singular: "louse", plural: "lice", exact: true}, - {singular: "mouse", plural: "mice"}, - {singular: "tooth", plural: "teeth"}, - {singular: "woman", plural: "women"}, - // misc https://en.wikipedia.org/wiki/English_plurals#Miscellaneous_irregular_plurals - {singular: "die", plural: "dice", exact: true}, - {singular: "person", plural: "people"}, - - // Words from French that end in -u add an x; in addition to eau to eaux rule - {singular: "adieu", plural: "adieux", alternative: "adieus"}, - {singular: "fabliau", plural: "fabliaux"}, - {singular: "bureau", plural: "bureaus", alternative: "bureaux"}, // popular - - // Words from Greek that end in -on change -on to -a; in addition to hedron rule - {singular: "criterion", plural: "criteria"}, - {singular: "ganglion", plural: "ganglia", alternative: "ganglions"}, - {singular: "lexicon", plural: "lexica", alternative: "lexicons"}, - {singular: "mitochondrion", plural: "mitochondria", alternative: "mitochondrions"}, - {singular: "noumenon", plural: "noumena"}, - {singular: "phenomenon", plural: "phenomena"}, - {singular: "taxon", plural: "taxa"}, - - // Words from Latin that end in -um change -um to -a; in addition to some rules - {singular: "media", plural: "media"}, // popular case: media -> media - {singular: "medium", plural: "media", alternative: "mediums", unidirectional: true}, - {singular: "stadium", plural: "stadiums", alternative: "stadia"}, - {singular: "aquarium", plural: "aquaria", alternative: "aquariums"}, - {singular: "auditorium", plural: "auditoria", alternative: "auditoriums"}, - {singular: "symposium", plural: "symposia", alternative: "symposiums"}, - {singular: "curriculum", plural: "curriculums", alternative: "curricula"}, // ulum - {singular: "quota", plural: "quotas"}, - - // Words from Latin that end in -us change -us to -i or -era - {singular: "alumnus", plural: "alumni", alternative: "alumnuses"}, // -i - {singular: "bacillus", plural: "bacilli"}, - {singular: "cactus", plural: "cacti", alternative: "cactuses"}, - {singular: "coccus", plural: "cocci"}, - {singular: "focus", plural: "foci", alternative: "focuses"}, - {singular: "locus", plural: "loci", alternative: "locuses"}, - {singular: "nucleus", plural: "nuclei", alternative: "nucleuses"}, - {singular: "octopus", plural: "octupuses", alternative: "octopi"}, - {singular: "radius", plural: "radii", alternative: "radiuses"}, - {singular: "syllabus", plural: "syllabi"}, - {singular: "corpus", plural: "corpora", alternative: "corpuses"}, // -ra - {singular: "genus", plural: "genera"}, - - // Words from Latin that end in -a change -a to -ae - {singular: "alumna", plural: "alumnae"}, - {singular: "vertebra", plural: "vertebrae"}, - {singular: "differentia", plural: "differentiae"}, // -tia - {singular: "minutia", plural: "minutiae"}, - {singular: "vita", plural: "vitae"}, // -ita - {singular: "larva", plural: "larvae"}, // -va - {singular: "postcava", plural: "postcavae"}, - {singular: "praecava", plural: "praecavae"}, - {singular: "uva", plural: "uvae"}, - - // Words from Latin that end in -ex change -ex to -ices - {singular: "apex", plural: "apices", alternative: "apexes"}, - {singular: "codex", plural: "codices", alternative: "codexes"}, - {singular: "index", plural: "indices", alternative: "indexes"}, - {singular: "latex", plural: "latices", alternative: "latexes"}, - {singular: "vertex", plural: "vertices", alternative: "vertexes"}, - {singular: "vortex", plural: "vortices", alternative: "vortexes"}, - - // Words from Latin that end in -ix change -ix to -ices (eg, matrix becomes matrices) - {singular: "appendix", plural: "appendices", alternative: "appendixes"}, - {singular: "radix", plural: "radices", alternative: "radixes"}, - {singular: "helix", plural: "helices", alternative: "helixes"}, - - // Words from Latin that end in -is change -is to -es - {singular: "axis", plural: "axes", exact: true}, - {singular: "crisis", plural: "crises"}, - {singular: "ellipsis", plural: "ellipses", unidirectional: true}, // ellipse - {singular: "genesis", plural: "geneses"}, - {singular: "oasis", plural: "oases"}, - {singular: "thesis", plural: "theses"}, - {singular: "testis", plural: "testes"}, - {singular: "base", plural: "bases"}, // popular case - {singular: "basis", plural: "bases", unidirectional: true}, - - {singular: "alias", plural: "aliases", exact: true}, // no alia, no aliasis - {singular: "vedalia", plural: "vedalias"}, // no vedalium, no vedaliases - - // Words that end in -ch, -o, -s, -sh, -x, -z (can be conflict with the others) - {singular: "use", plural: "uses", exact: true}, // us vs use - {singular: "abuse", plural: "abuses"}, - {singular: "cause", plural: "causes"}, - {singular: "clause", plural: "clauses"}, - {singular: "cruse", plural: "cruses"}, - {singular: "excuse", plural: "excuses"}, - {singular: "fuse", plural: "fuses"}, - {singular: "house", plural: "houses"}, - {singular: "misuse", plural: "misuses"}, - {singular: "muse", plural: "muses"}, - {singular: "pause", plural: "pauses"}, - {singular: "ache", plural: "aches"}, - {singular: "topaz", plural: "topazes"}, - {singular: "buffalo", plural: "buffaloes", alternative: "buffalos"}, - {singular: "potato", plural: "potatoes"}, - {singular: "tomato", plural: "tomatoes"}, - - // uncountables - {singular: "equipment", uncountable: true}, - {singular: "information", uncountable: true}, - {singular: "jeans", uncountable: true}, - {singular: "money", uncountable: true}, - {singular: "news", uncountable: true}, - {singular: "rice", uncountable: true}, - - // exceptions: -f to -ves, not -fe - {singular: "dwarf", plural: "dwarfs", alternative: "dwarves"}, - {singular: "hoof", plural: "hoofs", alternative: "hooves"}, - {singular: "thief", plural: "thieves"}, - // exceptions: instead of -f(e) to -ves - {singular: "chive", plural: "chives"}, - {singular: "hive", plural: "hives"}, - {singular: "move", plural: "moves"}, - - // exceptions: instead of -y to -ies - {singular: "movie", plural: "movies"}, - {singular: "cookie", plural: "cookies"}, - - // exceptions: instead of -um to -a - {singular: "pretorium", plural: "pretoriums"}, - {singular: "agenda", plural: "agendas"}, // instead of plural of agendum - // exceptions: instead of -um to -a (chemical element names) - - // Words from Latin that end in -a change -a to -ae - {singular: "formula", plural: "formulas", alternative: "formulae"}, // also -um/-a - - // exceptions: instead of -o to -oes - {singular: "shoe", plural: "shoes"}, - {singular: "toe", plural: "toes", exact: true}, - {singular: "graffiti", plural: "graffiti"}, - - // abbreviations - {singular: "ID", plural: "IDs", exact: true}, -} - -// singleToPlural is the highest priority map for Pluralize(). -// singularToPluralSuffixList is used to build pluralRules for suffixes and -// compound words. -var singleToPlural = map[string]string{} - -// pluralToSingle is the highest priority map for Singularize(). -// singularToPluralSuffixList is used to build singularRules for suffixes and -// compound words. -var pluralToSingle = map[string]string{} - -// NOTE: This map should not be built as reverse map of singleToPlural since -// there are words that has the same plurals. - -// build singleToPlural and pluralToSingle with dictionary -func init() { - for _, wd := range dictionary { - if singleToPlural[wd.singular] != "" { - panic(fmt.Errorf("map singleToPlural already has an entry for %s", wd.singular)) - } - - if wd.uncountable && wd.plural == "" { - wd.plural = wd.singular - } - - if wd.plural == "" { - panic(fmt.Errorf("plural for %s is not provided", wd.singular)) - } - - singleToPlural[wd.singular] = wd.plural - - if !wd.unidirectional { - if pluralToSingle[wd.plural] != "" { - panic(fmt.Errorf("map pluralToSingle already has an entry for %s", wd.plural)) - } - pluralToSingle[wd.plural] = wd.singular - - if wd.alternative != "" { - if pluralToSingle[wd.alternative] != "" { - panic(fmt.Errorf("map pluralToSingle already has an entry for %s", wd.alternative)) - } - pluralToSingle[wd.alternative] = wd.singular - } - } - } -} - -type singularToPluralSuffix struct { - singular string - plural string -} - -// singularToPluralSuffixList is a list of "bidirectional" suffix rules for -// the irregular plurals follow such rules. -// -// NOTE: IMPORTANT! The order of items in this list is the rule priority, not -// alphabet order. The first match will be used to inflect. -var singularToPluralSuffixList = []singularToPluralSuffix{ - // https://en.wiktionary.org/wiki/Appendix:English_irregular_nouns#Rules - // Words that end in -f or -fe change -f or -fe to -ves - {"tive", "tives"}, // exception - {"eaf", "eaves"}, - {"oaf", "oaves"}, - {"afe", "aves"}, - {"arf", "arves"}, - {"rfe", "rves"}, - {"rf", "rves"}, - {"lf", "lves"}, - {"fe", "ves"}, // previously '[a-eg-km-z]fe' TODO: regex support - - // Words that end in -y preceded by a consonant change -y to -ies - {"ay", "ays"}, - {"ey", "eys"}, - {"oy", "oys"}, - {"quy", "quies"}, - {"uy", "uys"}, - {"y", "ies"}, // '[^aeiou]y' - - // Words from French that end in -u add an x (eg, château becomes châteaux) - {"eau", "eaux"}, // it seems like 'eau' is the most popular form of this rule - - // Words from Latin that end in -a change -a to -ae; before -on to -a and -um to -a - {"bula", "bulae"}, - {"dula", "bulae"}, - {"lula", "bulae"}, - {"nula", "bulae"}, - {"vula", "bulae"}, - - // Words from Greek that end in -on change -on to -a (eg, polyhedron becomes polyhedra) - // https://en.wiktionary.org/wiki/Category:English_irregular_plurals_ending_in_"-a" - {"hedron", "hedra"}, - - // Words from Latin that end in -um change -um to -a (eg, minimum becomes minima) - // https://en.wiktionary.org/wiki/Category:English_irregular_plurals_ending_in_"-a" - {"ium", "ia"}, // some exceptions especially chemical element names - {"seum", "seums"}, - {"eum", "ea"}, - {"oum", "oa"}, - {"stracum", "straca"}, - {"dum", "da"}, - {"elum", "ela"}, - {"ilum", "ila"}, - {"olum", "ola"}, - {"ulum", "ula"}, - {"llum", "lla"}, - {"ylum", "yla"}, - {"imum", "ima"}, - {"ernum", "erna"}, - {"gnum", "gna"}, - {"brum", "bra"}, - {"crum", "cra"}, - {"terum", "tera"}, - {"serum", "sera"}, - {"trum", "tra"}, - {"antum", "anta"}, - {"atum", "ata"}, - {"entum", "enta"}, - {"etum", "eta"}, - {"itum", "ita"}, - {"otum", "ota"}, - {"utum", "uta"}, - {"ctum", "cta"}, - {"ovum", "ova"}, - - // Words from Latin that end in -us change -us to -i or -era - // not easy to make a simple rule. just add them all to the dictionary - - // Words from Latin that end in -ex change -ex to -ices (eg, vortex becomes vortices) - // Words from Latin that end in -ix change -ix to -ices (eg, matrix becomes matrices) - // for example, -dix, -dex, and -dice will have the same plural form so - // making a simple rule is not possible for them - {"trix", "trices"}, // ignore a few words end in trice - - // Words from Latin that end in -is change -is to -es (eg, thesis becomes theses) - // -sis and -se has the same plural -ses so making a rule is not easy too. - {"iasis", "iases"}, - {"mesis", "meses"}, - {"kinesis", "kineses"}, - {"resis", "reses"}, - {"gnosis", "gnoses"}, // e.g. diagnosis - {"opsis", "opses"}, // e.g. synopsis - {"ysis", "yses"}, // e.g. analysis - - // Words that end in -ch, -o, -s, -sh, -x, -z - {"ouse", "ouses"}, - {"lause", "lauses"}, - {"us", "uses"}, // use/uses is in the dictionary - - {"ch", "ches"}, - {"io", "ios"}, - {"sh", "shes"}, - {"ss", "sses"}, - {"ez", "ezzes"}, - {"iz", "izzes"}, - {"tz", "tzes"}, - {"zz", "zzes"}, - {"ano", "anos"}, - {"lo", "los"}, - {"to", "tos"}, - {"oo", "oos"}, - {"o", "oes"}, - {"x", "xes"}, - - // for abbreviations - {"S", "Ses"}, - - // excluded rules: seems rare - // Words from Hebrew that add -im or -ot (eg, cherub becomes cherubim) - // - cherub (cherubs or cherubim), seraph (seraphs or seraphim) - // Words from Greek that end in -ma change -ma to -mata - // - The most of words end in -ma are in this category but it looks like - // just adding -s is more popular. - // Words from Latin that end in -nx change -nx to -nges - // - The most of words end in -nx are in this category but it looks like - // just adding -es is more popular. (sphinxes) - - // excluded rules: don't care at least for now: - // Words that end in -ful that add an s after the -ful - // Words that end in -s or -ese denoting a national of a particular country - // Symbols or letters, which often add -'s -} - -func init() { - for i := len(singularToPluralSuffixList) - 1; i >= 0; i-- { - InsertPluralRule(singularToPluralSuffixList[i].singular, singularToPluralSuffixList[i].plural) - InsertSingularRule(singularToPluralSuffixList[i].plural, singularToPluralSuffixList[i].singular) - } - - // build pluralRule and singularRule with dictionary for compound words - for _, wd := range dictionary { - if wd.exact { - continue - } - - if wd.uncountable && wd.plural == "" { - wd.plural = wd.singular - } - - InsertPluralRule(wd.singular, wd.plural) - - if !wd.unidirectional { - InsertSingularRule(wd.plural, wd.singular) - - if wd.alternative != "" { - InsertSingularRule(wd.alternative, wd.singular) - } - } - } + core.InsertPluralRule(suffix, repl) } diff --git a/pluralize.go b/pluralize.go index d0ac77d..981218d 100644 --- a/pluralize.go +++ b/pluralize.go @@ -1,72 +1,20 @@ package flect -import ( - "strings" - "sync" -) - -var pluralMoot = &sync.RWMutex{} +import "github.com/gobuffalo/flect/internal/core" // Pluralize returns a plural version of the string +// // user = users // person = people // datum = data func Pluralize(s string) string { - return New(s).Pluralize().String() + return core.Pluralize(s) } -// PluralizeWithSize will pluralize a string taking a number number into account. +// PluralizeWithSize will pluralize a string taking a number into account. +// // PluralizeWithSize("user", 1) = user // PluralizeWithSize("user", 2) = users func PluralizeWithSize(s string, i int) string { - if i == 1 || i == -1 { - return New(s).Singularize().String() - } - return New(s).Pluralize().String() -} - -// Pluralize returns a plural version of the string -// user = users -// person = people -// datum = data -func (i Ident) Pluralize() Ident { - s := i.LastPart() - if len(s) == 0 { - return New("") - } - - pluralMoot.RLock() - defer pluralMoot.RUnlock() - - // check if the Original has an explicit entry in the map - if p, ok := singleToPlural[i.Original]; ok { - return i.ReplaceSuffix(i.Original, p) - } - if _, ok := pluralToSingle[i.Original]; ok { - return i - } - - ls := strings.ToLower(s) - if _, ok := pluralToSingle[ls]; ok { - return i - } - - if p, ok := singleToPlural[ls]; ok { - if s == Capitalize(s) { - p = Capitalize(p) - } - return i.ReplaceSuffix(s, p) - } - - for _, r := range pluralRules { - if strings.HasSuffix(s, r.suffix) { - return i.ReplaceSuffix(s, r.fn(s)) - } - } - - if strings.HasSuffix(ls, "s") { - return i - } - - return New(i.String() + "s") + return core.PluralizeWithSize(s, i) } diff --git a/pluralize_test.go b/pluralize_test.go index 0ec507c..09f409c 100644 --- a/pluralize_test.go +++ b/pluralize_test.go @@ -2,15 +2,13 @@ package flect import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_Pluralize(t *testing.T) { for _, tt := range singlePluralAssertions { if tt.doPluralizeTest { t.Run(tt.singular, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.plural, Pluralize(tt.singular), "pluralize %s", tt.singular) r.Equal(tt.plural, Pluralize(tt.plural), "pluralize %s", tt.plural) }) @@ -21,7 +19,7 @@ func Test_Pluralize(t *testing.T) { func Test_PluralizeWithSize(t *testing.T) { for _, tt := range singlePluralAssertions { t.Run(tt.singular, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) if tt.doSingularizeTest { r.Equal(tt.singular, PluralizeWithSize(tt.singular, -1), "pluralize %d %s", -1, tt.singular) r.Equal(tt.singular, PluralizeWithSize(tt.plural, -1), "pluralize %d %s", -1, tt.plural) diff --git a/rule.go b/rule.go index 5428e6e..ea7835f 100644 --- a/rule.go +++ b/rule.go @@ -1,17 +1 @@ package flect - -type ruleFn func(string) string - -type rule struct { - suffix string - fn ruleFn -} - -func simpleRuleFunc(suffix, repl string) func(string) string { - return func(s string) string { - s = s[:len(s)-len(suffix)] - return s + repl - } -} - -func noop(s string) string { return s } diff --git a/singular_rules.go b/singular_rules.go index d59307b..d3bdee1 100644 --- a/singular_rules.go +++ b/singular_rules.go @@ -1,26 +1,15 @@ package flect -var singularRules = []rule{} +import "github.com/gobuffalo/flect/internal/core" // AddSingular adds a rule that will replace the given suffix with the replacement suffix. // The name is confusing. This function will be deprecated in the next release. func AddSingular(ext string, repl string) { - InsertSingularRule(ext, repl) + core.AddSingular(ext, repl) } // InsertSingularRule inserts a rule that will replace the given suffix with // the repl(acement) at the beginning of the list of the singularize rules. func InsertSingularRule(suffix, repl string) { - singularMoot.Lock() - defer singularMoot.Unlock() - - singularRules = append([]rule{{ - suffix: suffix, - fn: simpleRuleFunc(suffix, repl), - }}, singularRules...) - - singularRules = append([]rule{{ - suffix: repl, - fn: noop, - }}, singularRules...) + core.InsertSingularRule(suffix, repl) } diff --git a/singularize.go b/singularize.go index d00cf4f..1e7e345 100644 --- a/singularize.go +++ b/singularize.go @@ -1,69 +1,20 @@ package flect -import ( - "strings" - "sync" -) - -var singularMoot = &sync.RWMutex{} +import "github.com/gobuffalo/flect/internal/core" // Singularize returns a singular version of the string +// // users = user // data = datum // people = person func Singularize(s string) string { - return New(s).Singularize().String() + return core.Singularize(s) } -// SingularizeWithSize will singular a string taking a number number into account. +// SingularizeWithSize will singularize a string taking a number into account. +// // SingularizeWithSize("user", 1) = user // SingularizeWithSize("user", 2) = users func SingularizeWithSize(s string, i int) string { - return PluralizeWithSize(s, i) -} - -// Singularize returns a singular version of the string -// users = user -// data = datum -// people = person -func (i Ident) Singularize() Ident { - s := i.LastPart() - if len(s) == 0 { - return i - } - - singularMoot.RLock() - defer singularMoot.RUnlock() - - // check if the Original has an explicit entry in the map - if p, ok := pluralToSingle[i.Original]; ok { - return i.ReplaceSuffix(i.Original, p) - } - if _, ok := singleToPlural[i.Original]; ok { - return i - } - - ls := strings.ToLower(s) - if p, ok := pluralToSingle[ls]; ok { - if s == Capitalize(s) { - p = Capitalize(p) - } - return i.ReplaceSuffix(s, p) - } - - if _, ok := singleToPlural[ls]; ok { - return i - } - - for _, r := range singularRules { - if strings.HasSuffix(s, r.suffix) { - return i.ReplaceSuffix(s, r.fn(s)) - } - } - - if strings.HasSuffix(s, "s") { - return i.ReplaceSuffix("s", "") - } - - return i + return core.SingularizeWithSize(s, i) } diff --git a/singularize_test.go b/singularize_test.go index 86f04c7..1bae6d4 100644 --- a/singularize_test.go +++ b/singularize_test.go @@ -2,15 +2,13 @@ package flect import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_Singularize(t *testing.T) { for _, tt := range singlePluralAssertions { if tt.doSingularizeTest { t.Run(tt.plural, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.singular, Singularize(tt.plural), "singularize %s", tt.plural) r.Equal(tt.singular, Singularize(tt.singular), "singularize %s", tt.singular) }) @@ -21,7 +19,7 @@ func Test_Singularize(t *testing.T) { func Test_SingularizeWithSize(t *testing.T) { for _, tt := range singlePluralAssertions { t.Run(tt.plural, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) if tt.doSingularizeTest { r.Equal(tt.singular, SingularizeWithSize(tt.plural, -1), "singularize %d %s", -1, tt.plural) r.Equal(tt.singular, SingularizeWithSize(tt.singular, -1), "singularize %d %s", -1, tt.singular) diff --git a/testhelpers_test.go b/testhelpers_test.go new file mode 100644 index 0000000..6e9ef89 --- /dev/null +++ b/testhelpers_test.go @@ -0,0 +1,13 @@ +package flect + +import ( + "testing" + + th "github.com/gobuffalo/flect/internal/testhelpers" +) + +type requireHelper = th.RequireHelper + +func newRequire(t testing.TB) *requireHelper { + return th.NewRequire(t) +} diff --git a/titleize.go b/titleize.go index 0878ada..f7e8ec6 100644 --- a/titleize.go +++ b/titleize.go @@ -1,38 +1,12 @@ package flect -import ( - "strings" - "unicode" -) +import "github.com/gobuffalo/flect/internal/core" // Titleize will capitalize the start of each part +// // "Nice to see you!" = "Nice To See You!" // "i've read a book! have you?" = "I've Read A Book! Have You?" // "This is `code` ok" = "This Is `code` OK" func Titleize(s string) string { - return New(s).Titleize().String() -} - -// Titleize will capitalize the start of each part -// "Nice to see you!" = "Nice To See You!" -// "i've read a book! have you?" = "I've Read A Book! Have You?" -// "This is `code` ok" = "This Is `code` OK" -func (i Ident) Titleize() Ident { - var parts []string - - // TODO: we need to reconsider the design. - // this approach preserves inline code block as is but it also - // preserves the other words start with a special character. - // I would prefer: "*wonderful* world" to be "*Wonderful* World" - for _, part := range i.Parts { - // CAUTION: in unicode, []rune(str)[0] is not rune(str[0]) - runes := []rune(part) - x := string(unicode.ToTitle(runes[0])) - if len(runes) > 1 { - x += string(runes[1:]) - } - parts = append(parts, x) - } - - return New(strings.Join(parts, " ")) + return core.Titleize(s) } diff --git a/titleize_test.go b/titleize_test.go index 8e6c44a..56f8198 100644 --- a/titleize_test.go +++ b/titleize_test.go @@ -2,8 +2,6 @@ package flect import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_Titleize(t *testing.T) { @@ -23,7 +21,7 @@ func Test_Titleize(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, Titleize(tt.act)) r.Equal(tt.exp, Titleize(tt.exp)) }) diff --git a/underscore.go b/underscore.go index d42859a..d66f2af 100644 --- a/underscore.go +++ b/underscore.go @@ -1,35 +1,12 @@ package flect -import ( - "strings" - "unicode" -) +import "github.com/gobuffalo/flect/internal/core" // Underscore a string +// // bob dylan --> bob_dylan // Nice to see you! --> nice_to_see_you // widgetID --> widget_id func Underscore(s string) string { - return New(s).Underscore().String() -} - -// Underscore a string -// bob dylan --> bob_dylan -// Nice to see you! --> nice_to_see_you -// widgetID --> widget_id -func (i Ident) Underscore() Ident { - out := make([]string, 0, len(i.Parts)) - for _, part := range i.Parts { - var x strings.Builder - x.Grow(len(part)) - for _, c := range part { - if unicode.IsLetter(c) || unicode.IsDigit(c) { - x.WriteRune(c) - } - } - if x.Len() > 0 { - out = append(out, x.String()) - } - } - return New(strings.ToLower(strings.Join(out, "_"))) + return core.Underscore(s) } diff --git a/underscore_test.go b/underscore_test.go index 7122e9d..d506d22 100644 --- a/underscore_test.go +++ b/underscore_test.go @@ -2,8 +2,6 @@ package flect import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_Underscore(t *testing.T) { @@ -21,7 +19,7 @@ func Test_Underscore(t *testing.T) { for _, tt := range table { t.Run(tt.act, func(st *testing.T) { - r := require.New(st) + r := newRequire(st) r.Equal(tt.exp, Underscore(tt.act)) r.Equal(tt.exp, Underscore(tt.exp)) }) From 988d6f9b4b878a3d5623ae064b5e8e5ade35e813 Mon Sep 17 00:00:00 2001 From: Stanislas Michalak Date: Wed, 1 Jul 2026 22:55:48 +0200 Subject: [PATCH 2/4] chore: make root aliases a global barrel file --- acronyms.go | 9 --- camelize.go | 12 ---- capitalize.go | 12 ---- custom_data.go | 21 ------ dasherize.go | 12 ---- flect.go | 168 ++++++++++++++++++++++++++++++++++++++++++++++ humanize.go | 16 ----- ident.go | 11 --- lower_upper.go | 1 - ordinalize.go | 12 ---- pascalize.go | 12 ---- plural_rules.go | 15 ----- pluralize.go | 20 ------ rule.go | 1 - singular_rules.go | 15 ----- singularize.go | 20 ------ titleize.go | 12 ---- underscore.go | 12 ---- version.go | 2 +- 19 files changed, 169 insertions(+), 214 deletions(-) delete mode 100644 acronyms.go delete mode 100644 camelize.go delete mode 100644 capitalize.go delete mode 100644 custom_data.go delete mode 100644 dasherize.go delete mode 100644 humanize.go delete mode 100644 ident.go delete mode 100644 lower_upper.go delete mode 100644 ordinalize.go delete mode 100644 pascalize.go delete mode 100644 plural_rules.go delete mode 100644 pluralize.go delete mode 100644 rule.go delete mode 100644 singular_rules.go delete mode 100644 singularize.go delete mode 100644 titleize.go delete mode 100644 underscore.go diff --git a/acronyms.go b/acronyms.go deleted file mode 100644 index b1da380..0000000 --- a/acronyms.go +++ /dev/null @@ -1,9 +0,0 @@ -package flect - -import "github.com/gobuffalo/flect/internal/core" - -// baseAcronyms and dictionary are the same references as in internal/core, -// kept here so that package-level tests in this package can access them -// without importing the internal package directly. -var baseAcronyms = core.BaseAcronyms -var dictionary = core.Dictionary diff --git a/camelize.go b/camelize.go deleted file mode 100644 index e3db062..0000000 --- a/camelize.go +++ /dev/null @@ -1,12 +0,0 @@ -package flect - -import "github.com/gobuffalo/flect/internal/core" - -// Camelize returns a camelize version of a string -// -// bob dylan = bobDylan -// widget_id = widgetID -// WidgetID = widgetID -func Camelize(s string) string { - return core.Camelize(s) -} diff --git a/capitalize.go b/capitalize.go deleted file mode 100644 index 45c00f2..0000000 --- a/capitalize.go +++ /dev/null @@ -1,12 +0,0 @@ -package flect - -import "github.com/gobuffalo/flect/internal/core" - -// Capitalize will cap the first letter of string -// -// user = User -// bob dylan = Bob dylan -// widget_id = Widget_id -func Capitalize(s string) string { - return core.Capitalize(s) -} diff --git a/custom_data.go b/custom_data.go deleted file mode 100644 index 79707dc..0000000 --- a/custom_data.go +++ /dev/null @@ -1,21 +0,0 @@ -package flect - -import ( - "io" - - "github.com/gobuffalo/flect/internal/core" -) - -// CustomDataParser are functions that parse data like acronyms or -// plurals in the shape of a io.Reader it receives. -type CustomDataParser = core.CustomDataParser - -// LoadAcronyms loads acronyms from io.Reader param -func LoadAcronyms(r io.Reader) error { - return core.LoadAcronyms(r) -} - -// LoadInflections loads rules from io.Reader param -func LoadInflections(r io.Reader) error { - return core.LoadInflections(r) -} diff --git a/dasherize.go b/dasherize.go deleted file mode 100644 index 12ea0a7..0000000 --- a/dasherize.go +++ /dev/null @@ -1,12 +0,0 @@ -package flect - -import "github.com/gobuffalo/flect/internal/core" - -// Dasherize returns an alphanumeric, lowercased, dashed string -// -// Donald E. Knuth = donald-e-knuth -// Test with + sign = test-with-sign -// admin/WidgetID = admin-widget-id -func Dasherize(s string) string { - return core.Dasherize(s) -} diff --git a/flect.go b/flect.go index a530417..5a34979 100644 --- a/flect.go +++ b/flect.go @@ -2,3 +2,171 @@ Package flect is a new inflection engine to replace [https://github.com/markbates/inflect](https://github.com/markbates/inflect) designed to be more modular, more readable, and easier to fix issues on than the original. */ package flect + +import ( + "io" + + "github.com/gobuffalo/flect/internal/core" +) + +// Ident represents the string and it's parts +type Ident = core.Ident + +// CustomDataParser are functions that parse data like acronyms or +// plurals in the shape of a io.Reader it receives. +type CustomDataParser = core.CustomDataParser + +// baseAcronyms and dictionary are the same references as in internal/core, +// kept here so that package-level tests in this package can access them +// without importing the internal package directly. +var baseAcronyms = core.BaseAcronyms +var dictionary = core.Dictionary + +// New creates a new Ident from the string +func New(s string) Ident { + return core.New(s) +} + +// Camelize returns a camelize version of a string +// +// bob dylan = bobDylan +// widget_id = widgetID +// WidgetID = widgetID +func Camelize(s string) string { + return core.Camelize(s) +} + +// Capitalize will cap the first letter of string +// +// user = User +// bob dylan = Bob dylan +// widget_id = Widget_id +func Capitalize(s string) string { + return core.Capitalize(s) +} + +// Dasherize returns an alphanumeric, lowercased, dashed string +// +// Donald E. Knuth = donald-e-knuth +// Test with + sign = test-with-sign +// admin/WidgetID = admin-widget-id +func Dasherize(s string) string { + return core.Dasherize(s) +} + +// Humanize returns first letter of sentence capitalized. +// Common acronyms are capitalized as well. +// Other capital letters in string are left as provided. +// +// employee_salary = Employee salary +// employee_id = employee ID +// employee_mobile_number = Employee mobile number +// first_Name = First Name +// firstName = First Name +func Humanize(s string) string { + return core.Humanize(s) +} + +// Ordinalize converts a number to an ordinal version +// +// 42 = 42nd +// 45 = 45th +// 1 = 1st +func Ordinalize(s string) string { + return core.Ordinalize(s) +} + +// Pascalize returns a string with each segment capitalized +// +// user = User +// bob dylan = BobDylan +// widget_id = WidgetID +func Pascalize(s string) string { + return core.Pascalize(s) +} + +// Pluralize returns a plural version of the string +// +// user = users +// person = people +// datum = data +func Pluralize(s string) string { + return core.Pluralize(s) +} + +// PluralizeWithSize will pluralize a string taking a number into account. +// +// PluralizeWithSize("user", 1) = user +// PluralizeWithSize("user", 2) = users +func PluralizeWithSize(s string, i int) string { + return core.PluralizeWithSize(s, i) +} + +// AddPlural adds a rule that will replace the given suffix with the replacement suffix. +// The name is confusing. This function will be deprecated in the next release. +func AddPlural(suffix string, repl string) { + core.AddPlural(suffix, repl) +} + +// InsertPluralRule inserts a rule that will replace the given suffix with +// the repl(acement) at the begining of the list of the pluralize rules. +func InsertPluralRule(suffix, repl string) { + core.InsertPluralRule(suffix, repl) +} + +// Singularize returns a singular version of the string +// +// users = user +// data = datum +// people = person +func Singularize(s string) string { + return core.Singularize(s) +} + +// SingularizeWithSize will singularize a string taking a number into account. +// +// SingularizeWithSize("user", 1) = user +// SingularizeWithSize("user", 2) = users +func SingularizeWithSize(s string, i int) string { + return core.SingularizeWithSize(s, i) +} + +// AddSingular adds a rule that will replace the given suffix with the replacement suffix. +// The name is confusing. This function will be deprecated in the next release. +func AddSingular(ext string, repl string) { + core.AddSingular(ext, repl) +} + +// InsertSingularRule inserts a rule that will replace the given suffix with +// the repl(acement) at the beginning of the list of the singularize rules. +func InsertSingularRule(suffix, repl string) { + core.InsertSingularRule(suffix, repl) +} + +// Titleize will capitalize the start of each part +// +// "Nice to see you!" = "Nice To See You!" +// "i've read a book! have you?" = "I've Read A Book! Have You?" +// "This is `code` ok" = "This Is `code` OK" +func Titleize(s string) string { + return core.Titleize(s) +} + +// Underscore a string +// +// bob dylan --> bob_dylan +// Nice to see you! --> nice_to_see_you +// widgetID --> widget_id +func Underscore(s string) string { + return core.Underscore(s) +} + +// LoadAcronyms loads acronyms from io.Reader param +func LoadAcronyms(r io.Reader) error { + return core.LoadAcronyms(r) +} + +// LoadInflections loads rules from io.Reader param +func LoadInflections(r io.Reader) error { + return core.LoadInflections(r) +} diff --git a/humanize.go b/humanize.go deleted file mode 100644 index 6fe9511..0000000 --- a/humanize.go +++ /dev/null @@ -1,16 +0,0 @@ -package flect - -import "github.com/gobuffalo/flect/internal/core" - -// Humanize returns first letter of sentence capitalized. -// Common acronyms are capitalized as well. -// Other capital letters in string are left as provided. -// -// employee_salary = Employee salary -// employee_id = employee ID -// employee_mobile_number = Employee mobile number -// first_Name = First Name -// firstName = First Name -func Humanize(s string) string { - return core.Humanize(s) -} diff --git a/ident.go b/ident.go deleted file mode 100644 index 91cb26d..0000000 --- a/ident.go +++ /dev/null @@ -1,11 +0,0 @@ -package flect - -import "github.com/gobuffalo/flect/internal/core" - -// Ident represents the string and it's parts -type Ident = core.Ident - -// New creates a new Ident from the string -func New(s string) Ident { - return core.New(s) -} diff --git a/lower_upper.go b/lower_upper.go deleted file mode 100644 index ea7835f..0000000 --- a/lower_upper.go +++ /dev/null @@ -1 +0,0 @@ -package flect diff --git a/ordinalize.go b/ordinalize.go deleted file mode 100644 index dfc0a9a..0000000 --- a/ordinalize.go +++ /dev/null @@ -1,12 +0,0 @@ -package flect - -import "github.com/gobuffalo/flect/internal/core" - -// Ordinalize converts a number to an ordinal version -// -// 42 = 42nd -// 45 = 45th -// 1 = 1st -func Ordinalize(s string) string { - return core.Ordinalize(s) -} diff --git a/pascalize.go b/pascalize.go deleted file mode 100644 index c5b2c86..0000000 --- a/pascalize.go +++ /dev/null @@ -1,12 +0,0 @@ -package flect - -import "github.com/gobuffalo/flect/internal/core" - -// Pascalize returns a string with each segment capitalized -// -// user = User -// bob dylan = BobDylan -// widget_id = WidgetID -func Pascalize(s string) string { - return core.Pascalize(s) -} diff --git a/plural_rules.go b/plural_rules.go deleted file mode 100644 index 7044c14..0000000 --- a/plural_rules.go +++ /dev/null @@ -1,15 +0,0 @@ -package flect - -import "github.com/gobuffalo/flect/internal/core" - -// AddPlural adds a rule that will replace the given suffix with the replacement suffix. -// The name is confusing. This function will be deprecated in the next release. -func AddPlural(suffix string, repl string) { - core.AddPlural(suffix, repl) -} - -// InsertPluralRule inserts a rule that will replace the given suffix with -// the repl(acement) at the begining of the list of the pluralize rules. -func InsertPluralRule(suffix, repl string) { - core.InsertPluralRule(suffix, repl) -} diff --git a/pluralize.go b/pluralize.go deleted file mode 100644 index 981218d..0000000 --- a/pluralize.go +++ /dev/null @@ -1,20 +0,0 @@ -package flect - -import "github.com/gobuffalo/flect/internal/core" - -// Pluralize returns a plural version of the string -// -// user = users -// person = people -// datum = data -func Pluralize(s string) string { - return core.Pluralize(s) -} - -// PluralizeWithSize will pluralize a string taking a number into account. -// -// PluralizeWithSize("user", 1) = user -// PluralizeWithSize("user", 2) = users -func PluralizeWithSize(s string, i int) string { - return core.PluralizeWithSize(s, i) -} diff --git a/rule.go b/rule.go deleted file mode 100644 index ea7835f..0000000 --- a/rule.go +++ /dev/null @@ -1 +0,0 @@ -package flect diff --git a/singular_rules.go b/singular_rules.go deleted file mode 100644 index d3bdee1..0000000 --- a/singular_rules.go +++ /dev/null @@ -1,15 +0,0 @@ -package flect - -import "github.com/gobuffalo/flect/internal/core" - -// AddSingular adds a rule that will replace the given suffix with the replacement suffix. -// The name is confusing. This function will be deprecated in the next release. -func AddSingular(ext string, repl string) { - core.AddSingular(ext, repl) -} - -// InsertSingularRule inserts a rule that will replace the given suffix with -// the repl(acement) at the beginning of the list of the singularize rules. -func InsertSingularRule(suffix, repl string) { - core.InsertSingularRule(suffix, repl) -} diff --git a/singularize.go b/singularize.go deleted file mode 100644 index 1e7e345..0000000 --- a/singularize.go +++ /dev/null @@ -1,20 +0,0 @@ -package flect - -import "github.com/gobuffalo/flect/internal/core" - -// Singularize returns a singular version of the string -// -// users = user -// data = datum -// people = person -func Singularize(s string) string { - return core.Singularize(s) -} - -// SingularizeWithSize will singularize a string taking a number into account. -// -// SingularizeWithSize("user", 1) = user -// SingularizeWithSize("user", 2) = users -func SingularizeWithSize(s string, i int) string { - return core.SingularizeWithSize(s, i) -} diff --git a/titleize.go b/titleize.go deleted file mode 100644 index f7e8ec6..0000000 --- a/titleize.go +++ /dev/null @@ -1,12 +0,0 @@ -package flect - -import "github.com/gobuffalo/flect/internal/core" - -// Titleize will capitalize the start of each part -// -// "Nice to see you!" = "Nice To See You!" -// "i've read a book! have you?" = "I've Read A Book! Have You?" -// "This is `code` ok" = "This Is `code` OK" -func Titleize(s string) string { - return core.Titleize(s) -} diff --git a/underscore.go b/underscore.go deleted file mode 100644 index d66f2af..0000000 --- a/underscore.go +++ /dev/null @@ -1,12 +0,0 @@ -package flect - -import "github.com/gobuffalo/flect/internal/core" - -// Underscore a string -// -// bob dylan --> bob_dylan -// Nice to see you! --> nice_to_see_you -// widgetID --> widget_id -func Underscore(s string) string { - return core.Underscore(s) -} diff --git a/version.go b/version.go index 79486ed..5dad993 100644 --- a/version.go +++ b/version.go @@ -1,4 +1,4 @@ package flect -//Version holds Flect version number +// Version holds Flect version number const Version = "v1.0.0" From 614cff1e99ffb674cdc850c3d76d4dda28db2e2a Mon Sep 17 00:00:00 2001 From: Stanislas Michalak Date: Wed, 1 Jul 2026 23:03:33 +0200 Subject: [PATCH 3/4] chore: rename core pkg to flect --- flect.go | 2 +- internal/{core => flect}/acronyms.go | 2 +- internal/{core => flect}/camelize.go | 2 +- internal/{core => flect}/capitalize.go | 2 +- internal/{core => flect}/core.go | 2 +- internal/{core => flect}/custom_data.go | 2 +- internal/{core => flect}/dasherize.go | 2 +- internal/{core => flect}/humanize.go | 2 +- internal/{core => flect}/ident.go | 2 +- internal/{core => flect}/lower_upper.go | 2 +- internal/{core => flect}/ordinalize.go | 2 +- internal/{core => flect}/pascalize.go | 2 +- internal/{core => flect}/plural_rules.go | 2 +- internal/{core => flect}/pluralize.go | 2 +- internal/{core => flect}/rule.go | 2 +- internal/{core => flect}/singular_rules.go | 2 +- internal/{core => flect}/singularize.go | 2 +- internal/{core => flect}/titleize.go | 2 +- internal/{core => flect}/underscore.go | 2 +- name/file.go | 2 +- name/folder.go | 2 +- name/ident.go | 2 +- name/name.go | 2 +- 23 files changed, 23 insertions(+), 23 deletions(-) rename internal/{core => flect}/acronyms.go (99%) rename internal/{core => flect}/camelize.go (98%) rename internal/{core => flect}/capitalize.go (97%) rename internal/{core => flect}/core.go (96%) rename internal/{core => flect}/custom_data.go (99%) rename internal/{core => flect}/dasherize.go (98%) rename internal/{core => flect}/humanize.go (98%) rename internal/{core => flect}/ident.go (99%) rename internal/{core => flect}/lower_upper.go (95%) rename internal/{core => flect}/ordinalize.go (97%) rename internal/{core => flect}/pascalize.go (97%) rename internal/{core => flect}/plural_rules.go (99%) rename internal/{core => flect}/pluralize.go (99%) rename internal/{core => flect}/rule.go (95%) rename internal/{core => flect}/singular_rules.go (98%) rename internal/{core => flect}/singularize.go (99%) rename internal/{core => flect}/titleize.go (98%) rename internal/{core => flect}/underscore.go (98%) diff --git a/flect.go b/flect.go index 5a34979..dde5069 100644 --- a/flect.go +++ b/flect.go @@ -6,7 +6,7 @@ package flect import ( "io" - "github.com/gobuffalo/flect/internal/core" + core "github.com/gobuffalo/flect/internal/flect" ) // Ident represents the string and it's parts diff --git a/internal/core/acronyms.go b/internal/flect/acronyms.go similarity index 99% rename from internal/core/acronyms.go rename to internal/flect/acronyms.go index f3ede5e..54389e3 100644 --- a/internal/core/acronyms.go +++ b/internal/flect/acronyms.go @@ -1,4 +1,4 @@ -package core +package flect import "sync" diff --git a/internal/core/camelize.go b/internal/flect/camelize.go similarity index 98% rename from internal/core/camelize.go rename to internal/flect/camelize.go index d65a46c..fa73ea9 100644 --- a/internal/core/camelize.go +++ b/internal/flect/camelize.go @@ -1,4 +1,4 @@ -package core +package flect import ( "strings" diff --git a/internal/core/capitalize.go b/internal/flect/capitalize.go similarity index 97% rename from internal/core/capitalize.go rename to internal/flect/capitalize.go index 08a0461..fda355c 100644 --- a/internal/core/capitalize.go +++ b/internal/flect/capitalize.go @@ -1,4 +1,4 @@ -package core +package flect import "unicode" diff --git a/internal/core/core.go b/internal/flect/core.go similarity index 96% rename from internal/core/core.go rename to internal/flect/core.go index b259d29..54357e8 100644 --- a/internal/core/core.go +++ b/internal/flect/core.go @@ -1,4 +1,4 @@ -package core +package flect import ( "strings" diff --git a/internal/core/custom_data.go b/internal/flect/custom_data.go similarity index 99% rename from internal/core/custom_data.go rename to internal/flect/custom_data.go index 7594cd0..b108780 100644 --- a/internal/core/custom_data.go +++ b/internal/flect/custom_data.go @@ -1,4 +1,4 @@ -package core +package flect import ( "bytes" diff --git a/internal/core/dasherize.go b/internal/flect/dasherize.go similarity index 98% rename from internal/core/dasherize.go rename to internal/flect/dasherize.go index 4b42cfc..cab7fc8 100644 --- a/internal/core/dasherize.go +++ b/internal/flect/dasherize.go @@ -1,4 +1,4 @@ -package core +package flect import ( "strings" diff --git a/internal/core/humanize.go b/internal/flect/humanize.go similarity index 98% rename from internal/core/humanize.go rename to internal/flect/humanize.go index 0f7b07b..008c15d 100644 --- a/internal/core/humanize.go +++ b/internal/flect/humanize.go @@ -1,4 +1,4 @@ -package core +package flect import ( "strings" diff --git a/internal/core/ident.go b/internal/flect/ident.go similarity index 99% rename from internal/core/ident.go rename to internal/flect/ident.go index c5cd344..4dce83e 100644 --- a/internal/core/ident.go +++ b/internal/flect/ident.go @@ -1,4 +1,4 @@ -package core +package flect import ( "encoding" diff --git a/internal/core/lower_upper.go b/internal/flect/lower_upper.go similarity index 95% rename from internal/core/lower_upper.go rename to internal/flect/lower_upper.go index bf448c7..617e634 100644 --- a/internal/core/lower_upper.go +++ b/internal/flect/lower_upper.go @@ -1,4 +1,4 @@ -package core +package flect import "strings" diff --git a/internal/core/ordinalize.go b/internal/flect/ordinalize.go similarity index 97% rename from internal/core/ordinalize.go rename to internal/flect/ordinalize.go index f4030eb..f63ac6b 100644 --- a/internal/core/ordinalize.go +++ b/internal/flect/ordinalize.go @@ -1,4 +1,4 @@ -package core +package flect import "strconv" diff --git a/internal/core/pascalize.go b/internal/flect/pascalize.go similarity index 97% rename from internal/core/pascalize.go rename to internal/flect/pascalize.go index 061ec91..da2ede5 100644 --- a/internal/core/pascalize.go +++ b/internal/flect/pascalize.go @@ -1,4 +1,4 @@ -package core +package flect import ( "strings" diff --git a/internal/core/plural_rules.go b/internal/flect/plural_rules.go similarity index 99% rename from internal/core/plural_rules.go rename to internal/flect/plural_rules.go index 327daed..b8d9aa4 100644 --- a/internal/core/plural_rules.go +++ b/internal/flect/plural_rules.go @@ -1,4 +1,4 @@ -package core +package flect import "fmt" diff --git a/internal/core/pluralize.go b/internal/flect/pluralize.go similarity index 99% rename from internal/core/pluralize.go rename to internal/flect/pluralize.go index f478ccc..158bf3b 100644 --- a/internal/core/pluralize.go +++ b/internal/flect/pluralize.go @@ -1,4 +1,4 @@ -package core +package flect import ( "strings" diff --git a/internal/core/rule.go b/internal/flect/rule.go similarity index 95% rename from internal/core/rule.go rename to internal/flect/rule.go index df20398..5428e6e 100644 --- a/internal/core/rule.go +++ b/internal/flect/rule.go @@ -1,4 +1,4 @@ -package core +package flect type ruleFn func(string) string diff --git a/internal/core/singular_rules.go b/internal/flect/singular_rules.go similarity index 98% rename from internal/core/singular_rules.go rename to internal/flect/singular_rules.go index ccb45de..d59307b 100644 --- a/internal/core/singular_rules.go +++ b/internal/flect/singular_rules.go @@ -1,4 +1,4 @@ -package core +package flect var singularRules = []rule{} diff --git a/internal/core/singularize.go b/internal/flect/singularize.go similarity index 99% rename from internal/core/singularize.go rename to internal/flect/singularize.go index ca66c73..9f1bfd6 100644 --- a/internal/core/singularize.go +++ b/internal/flect/singularize.go @@ -1,4 +1,4 @@ -package core +package flect import ( "strings" diff --git a/internal/core/titleize.go b/internal/flect/titleize.go similarity index 98% rename from internal/core/titleize.go rename to internal/flect/titleize.go index a265480..815d307 100644 --- a/internal/core/titleize.go +++ b/internal/flect/titleize.go @@ -1,4 +1,4 @@ -package core +package flect import ( "strings" diff --git a/internal/core/underscore.go b/internal/flect/underscore.go similarity index 98% rename from internal/core/underscore.go rename to internal/flect/underscore.go index e2f2992..6521766 100644 --- a/internal/core/underscore.go +++ b/internal/flect/underscore.go @@ -1,4 +1,4 @@ -package core +package flect import ( "strings" diff --git a/name/file.go b/name/file.go index 5eebdb2..92e42b3 100644 --- a/name/file.go +++ b/name/file.go @@ -3,7 +3,7 @@ package name import ( "strings" - core "github.com/gobuffalo/flect/internal/core" + core "github.com/gobuffalo/flect/internal/flect" ) // File creates a suitable file name diff --git a/name/folder.go b/name/folder.go index 7b95f8f..b1e8fd5 100644 --- a/name/folder.go +++ b/name/folder.go @@ -4,7 +4,7 @@ import ( "regexp" "strings" - core "github.com/gobuffalo/flect/internal/core" + core "github.com/gobuffalo/flect/internal/flect" ) var alphanum = regexp.MustCompile(`[^a-zA-Z0-9_]+`) diff --git a/name/ident.go b/name/ident.go index 1204ae4..792f433 100644 --- a/name/ident.go +++ b/name/ident.go @@ -1,6 +1,6 @@ package name -import core "github.com/gobuffalo/flect/internal/core" +import core "github.com/gobuffalo/flect/internal/flect" // Ident represents the string and it's parts type Ident struct { diff --git a/name/name.go b/name/name.go index ca910d5..4b91bc5 100644 --- a/name/name.go +++ b/name/name.go @@ -4,7 +4,7 @@ import ( "encoding" "strings" - core "github.com/gobuffalo/flect/internal/core" + core "github.com/gobuffalo/flect/internal/flect" ) // Proper pascalizes and singularizes the string From 7c06a43dbb7cf379812bc5a27d93b1d533c743da Mon Sep 17 00:00:00 2001 From: Stanislas Michalak Date: Wed, 1 Jul 2026 23:08:46 +0200 Subject: [PATCH 4/4] feat: acronyms map value takes space for nothing We only need to check for existance of the key, map[string]struct{} takes no space to store the value. --- flect_test.go | 3 +- internal/flect/acronyms.go | 290 +++++++++++++++++----------------- internal/flect/core.go | 3 +- internal/flect/custom_data.go | 2 +- internal/flect/ident.go | 5 +- underscore_test.go | 2 +- 6 files changed, 154 insertions(+), 151 deletions(-) diff --git a/flect_test.go b/flect_test.go index 44477f0..09e1013 100644 --- a/flect_test.go +++ b/flect_test.go @@ -69,7 +69,8 @@ func Test_LoadAcronyms(t *testing.T) { r.NoError(LoadAcronyms(bytes.NewReader(b))) for _, acronym := range m { - r.True(baseAcronyms[acronym]) + _, ok := baseAcronyms[acronym] + r.True(ok) } } diff --git a/internal/flect/acronyms.go b/internal/flect/acronyms.go index 54389e3..57e2de4 100644 --- a/internal/flect/acronyms.go +++ b/internal/flect/acronyms.go @@ -4,149 +4,149 @@ import "sync" var acronymsMoot = &sync.RWMutex{} -var BaseAcronyms = map[string]bool{ - "OK": true, - "UTF8": true, - "HTML": true, - "JSON": true, - "JWT": true, - "ID": true, - "UUID": true, - "SQL": true, - "ACK": true, - "ACL": true, - "ADSL": true, - "AES": true, - "ANSI": true, - "API": true, - "ARP": true, - "ATM": true, - "BGP": true, - "BSS": true, - "CCITT": true, - "CHAP": true, - "CIDR": true, - "CIR": true, - "CLI": true, - "CPE": true, - "CPU": true, - "CRC": true, - "CRT": true, - "CSMA": true, - "CMOS": true, - "DCE": true, - "DEC": true, - "DES": true, - "DHCP": true, - "DNS": true, - "DRAM": true, - "DSL": true, - "DSLAM": true, - "DTE": true, - "DMI": true, - "EHA": true, - "EIA": true, - "EIGRP": true, - "EOF": true, - "ESS": true, - "FCC": true, - "FCS": true, - "FDDI": true, - "FTP": true, - "GBIC": true, - "gbps": true, - "GEPOF": true, - "HDLC": true, - "HTTP": true, - "HTTPS": true, - "IANA": true, - "ICMP": true, - "IDF": true, - "IDS": true, - "IEEE": true, - "IETF": true, - "IMAP": true, - "IP": true, - "IPS": true, - "ISDN": true, - "ISP": true, - "kbps": true, - "LACP": true, - "LAN": true, - "LAPB": true, - "LAPF": true, - "LLC": true, - "MAC": true, - "Mbps": true, - "MC": true, - "MDF": true, - "MIB": true, - "MoCA": true, - "MPLS": true, - "MTU": true, - "NAC": true, - "NAT": true, - "NBMA": true, - "NIC": true, - "NRZ": true, - "NRZI": true, - "NVRAM": true, - "OSI": true, - "OSPF": true, - "OUI": true, - "PAP": true, - "PAT": true, - "PC": true, - "PIM": true, - "PCM": true, - "PDU": true, - "POP3": true, - "POTS": true, - "PPP": true, - "PPTP": true, - "PTT": true, - "PVST": true, - "RAM": true, - "RARP": true, - "RFC": true, - "RIP": true, - "RLL": true, - "ROM": true, - "RSTP": true, - "RTP": true, - "RCP": true, - "SDLC": true, - "SFD": true, - "SFP": true, - "SLARP": true, - "SLIP": true, - "SMTP": true, - "SNA": true, - "SNAP": true, - "SNMP": true, - "SOF": true, - "SRAM": true, - "SSH": true, - "SSID": true, - "STP": true, - "SYN": true, - "TDM": true, - "TFTP": true, - "TIA": true, - "TOFU": true, - "UDP": true, - "URL": true, - "URI": true, - "USB": true, - "UTP": true, - "VC": true, - "VLAN": true, - "VLSM": true, - "VPN": true, - "W3C": true, - "WAN": true, - "WEP": true, - "WiFi": true, - "WPA": true, - "WWW": true, +var BaseAcronyms = map[string]struct{}{ + "OK": {}, + "UTF8": {}, + "HTML": {}, + "JSON": {}, + "JWT": {}, + "ID": {}, + "UUID": {}, + "SQL": {}, + "ACK": {}, + "ACL": {}, + "ADSL": {}, + "AES": {}, + "ANSI": {}, + "API": {}, + "ARP": {}, + "ATM": {}, + "BGP": {}, + "BSS": {}, + "CCITT": {}, + "CHAP": {}, + "CIDR": {}, + "CIR": {}, + "CLI": {}, + "CPE": {}, + "CPU": {}, + "CRC": {}, + "CRT": {}, + "CSMA": {}, + "CMOS": {}, + "DCE": {}, + "DEC": {}, + "DES": {}, + "DHCP": {}, + "DNS": {}, + "DRAM": {}, + "DSL": {}, + "DSLAM": {}, + "DTE": {}, + "DMI": {}, + "EHA": {}, + "EIA": {}, + "EIGRP": {}, + "EOF": {}, + "ESS": {}, + "FCC": {}, + "FCS": {}, + "FDDI": {}, + "FTP": {}, + "GBIC": {}, + "gbps": {}, + "GEPOF": {}, + "HDLC": {}, + "HTTP": {}, + "HTTPS": {}, + "IANA": {}, + "ICMP": {}, + "IDF": {}, + "IDS": {}, + "IEEE": {}, + "IETF": {}, + "IMAP": {}, + "IP": {}, + "IPS": {}, + "ISDN": {}, + "ISP": {}, + "kbps": {}, + "LACP": {}, + "LAN": {}, + "LAPB": {}, + "LAPF": {}, + "LLC": {}, + "MAC": {}, + "Mbps": {}, + "MC": {}, + "MDF": {}, + "MIB": {}, + "MoCA": {}, + "MPLS": {}, + "MTU": {}, + "NAC": {}, + "NAT": {}, + "NBMA": {}, + "NIC": {}, + "NRZ": {}, + "NRZI": {}, + "NVRAM": {}, + "OSI": {}, + "OSPF": {}, + "OUI": {}, + "PAP": {}, + "PAT": {}, + "PC": {}, + "PIM": {}, + "PCM": {}, + "PDU": {}, + "POP3": {}, + "POTS": {}, + "PPP": {}, + "PPTP": {}, + "PTT": {}, + "PVST": {}, + "RAM": {}, + "RARP": {}, + "RFC": {}, + "RIP": {}, + "RLL": {}, + "ROM": {}, + "RSTP": {}, + "RTP": {}, + "RCP": {}, + "SDLC": {}, + "SFD": {}, + "SFP": {}, + "SLARP": {}, + "SLIP": {}, + "SMTP": {}, + "SNA": {}, + "SNAP": {}, + "SNMP": {}, + "SOF": {}, + "SRAM": {}, + "SSH": {}, + "SSID": {}, + "STP": {}, + "SYN": {}, + "TDM": {}, + "TFTP": {}, + "TIA": {}, + "TOFU": {}, + "UDP": {}, + "URL": {}, + "URI": {}, + "USB": {}, + "UTP": {}, + "VC": {}, + "VLAN": {}, + "VLSM": {}, + "VPN": {}, + "W3C": {}, + "WAN": {}, + "WEP": {}, + "WiFi": {}, + "WPA": {}, + "WWW": {}, } diff --git a/internal/flect/core.go b/internal/flect/core.go index 54357e8..b00f6ee 100644 --- a/internal/flect/core.go +++ b/internal/flect/core.go @@ -16,7 +16,8 @@ func isSpace(c rune) bool { func xappend(a []string, ss ...string) []string { for _, s := range ss { s = strings.TrimFunc(s, isSpace) - if up := strings.ToUpper(s); BaseAcronyms[up] { + up := strings.ToUpper(s) + if _, ok := BaseAcronyms[up]; ok { s = up } if s != "" { diff --git a/internal/flect/custom_data.go b/internal/flect/custom_data.go index b108780..f9ff104 100644 --- a/internal/flect/custom_data.go +++ b/internal/flect/custom_data.go @@ -54,7 +54,7 @@ func LoadAcronyms(r io.Reader) error { defer acronymsMoot.Unlock() for _, acronym := range m { - BaseAcronyms[acronym] = true + BaseAcronyms[acronym] = struct{}{} } return nil diff --git a/internal/flect/ident.go b/internal/flect/ident.go index 4dce83e..35bdbee 100644 --- a/internal/flect/ident.go +++ b/internal/flect/ident.go @@ -34,7 +34,8 @@ func toParts(s string) []string { if len(s) == 0 { return parts } - if up := strings.ToUpper(s); BaseAcronyms[up] { + up := strings.ToUpper(s) + if _, ok := BaseAcronyms[up]; ok { return []string{up} } var prev rune @@ -60,7 +61,7 @@ func toParts(s string) []string { prev = c continue } - if unicode.IsUpper(c) && BaseAcronyms[x.String()] { + if _, isAcronym := BaseAcronyms[x.String()]; unicode.IsUpper(c) && isAcronym { parts = xappend(parts, x.String()) x.Reset() x.WriteRune(c) diff --git a/underscore_test.go b/underscore_test.go index d506d22..ca31747 100644 --- a/underscore_test.go +++ b/underscore_test.go @@ -5,7 +5,7 @@ import ( ) func Test_Underscore(t *testing.T) { - baseAcronyms["TLC"] = true + baseAcronyms["TLC"] = struct{}{} table := []tt{ {"", ""},