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 deleted file mode 100644 index b169724..0000000 --- a/acronyms.go +++ /dev/null @@ -1,152 +0,0 @@ -package flect - -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/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_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_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/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..dde5069 100644 --- a/flect.go +++ b/flect.go @@ -4,40 +4,169 @@ Package flect is a new inflection engine to replace [https://github.com/markbate package flect import ( - "strings" - "unicode" + "io" + + core "github.com/gobuffalo/flect/internal/flect" ) -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 +// 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/flect_test.go b/flect_test.go index 4ab6251..09e1013 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", @@ -71,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) } } @@ -348,20 +347,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_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_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/flect/acronyms.go b/internal/flect/acronyms.go new file mode 100644 index 0000000..57e2de4 --- /dev/null +++ b/internal/flect/acronyms.go @@ -0,0 +1,152 @@ +package flect + +import "sync" + +var acronymsMoot = &sync.RWMutex{} + +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/camelize.go b/internal/flect/camelize.go similarity index 74% rename from camelize.go rename to internal/flect/camelize.go index d8851c8..fa73ea9 100644 --- a/camelize.go +++ b/internal/flect/camelize.go @@ -6,6 +6,7 @@ import ( ) // Camelize returns a camelize version of a string +// // bob dylan = bobDylan // widget_id = widgetID // WidgetID = widgetID @@ -14,31 +15,32 @@ func Camelize(s string) 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 x strings.Builder var capped bool for _, c := range part { if unicode.IsLetter(c) || unicode.IsDigit(c) { if i == 0 { - x += string(unicode.ToLower(c)) + x.WriteRune(unicode.ToLower(c)) continue } if !capped { capped = true - x += string(unicode.ToUpper(c)) + x.WriteRune(unicode.ToUpper(c)) continue } - x += string(c) + x.WriteRune(c) } } - if x != "" { - out = append(out, x) + if x.Len() > 0 { + out = append(out, x.String()) } } - return New(strings.Join(out, "")) + return Ident{Original: strings.Join(out, "")} } diff --git a/capitalize.go b/internal/flect/capitalize.go similarity index 88% rename from capitalize.go rename to internal/flect/capitalize.go index 78334fc..fda355c 100644 --- a/capitalize.go +++ b/internal/flect/capitalize.go @@ -3,6 +3,7 @@ package flect import "unicode" // Capitalize will cap the first letter of string +// // user = User // bob dylan = Bob dylan // widget_id = Widget_id @@ -11,14 +12,15 @@ func Capitalize(s string) 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("") + return Ident{} } runes := []rune(i.Original) runes[0] = unicode.ToTitle(runes[0]) - return New(string(runes)) + return Ident{Original: string(runes)} } diff --git a/internal/flect/core.go b/internal/flect/core.go new file mode 100644 index 0000000..b00f6ee --- /dev/null +++ b/internal/flect/core.go @@ -0,0 +1,35 @@ +package flect + +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) + up := strings.ToUpper(s) + if _, ok := BaseAcronyms[up]; ok { + 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/custom_data.go b/internal/flect/custom_data.go similarity index 85% rename from custom_data.go rename to internal/flect/custom_data.go index efb445f..f9ff104 100644 --- a/custom_data.go +++ b/internal/flect/custom_data.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "os" "path/filepath" "strings" @@ -16,8 +15,8 @@ func init() { 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. +// 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) { @@ -31,7 +30,7 @@ func loadCustomData(defaultFile, env, readErrorMessage string, parser CustomData return } - b, err := ioutil.ReadFile(path) + b, err := os.ReadFile(path) if err != nil { fmt.Printf("%s %s (%s)\n", readErrorMessage, path, err) return @@ -42,7 +41,7 @@ func loadCustomData(defaultFile, env, readErrorMessage string, parser CustomData } } -//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) @@ -55,13 +54,13 @@ func LoadAcronyms(r io.Reader) error { defer acronymsMoot.Unlock() for _, acronym := range m { - baseAcronyms[acronym] = true + BaseAcronyms[acronym] = struct{}{} } return nil } -//LoadInflections loads rules from io.Reader param +// LoadInflections loads rules from io.Reader param func LoadInflections(r io.Reader) error { m := map[string]string{} diff --git a/dasherize.go b/internal/flect/dasherize.go similarity index 80% rename from dasherize.go rename to internal/flect/dasherize.go index c7a8a33..cab7fc8 100644 --- a/dasherize.go +++ b/internal/flect/dasherize.go @@ -6,6 +6,7 @@ import ( ) // Dasherize returns an alphanumeric, lowercased, dashed string +// // Donald E. Knuth = donald-e-knuth // Test with + sign = test-with-sign // admin/WidgetID = admin-widget-id @@ -14,6 +15,7 @@ func Dasherize(s string) 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 @@ -21,14 +23,14 @@ func (i Ident) Dasherize() Ident { var parts []string for _, part := range i.Parts { - var x string + var x strings.Builder for _, c := range part { if unicode.IsLetter(c) || unicode.IsDigit(c) { - x += string(c) + x.WriteRune(c) } } - parts = xappend(parts, x) + parts = xappend(parts, x.String()) } - return New(strings.ToLower(strings.Join(parts, "-"))) + return Ident{Original: strings.ToLower(strings.Join(parts, "-"))} } diff --git a/humanize.go b/internal/flect/humanize.go similarity index 91% rename from humanize.go rename to internal/flect/humanize.go index 5100bfb..008c15d 100644 --- a/humanize.go +++ b/internal/flect/humanize.go @@ -20,7 +20,7 @@ func Humanize(s string) string { // Humanize First letter of sentence capitalized func (i Ident) Humanize() Ident { if len(i.Original) == 0 { - return New("") + return Ident{} } if strings.TrimSpace(i.Original) == "" { @@ -32,5 +32,5 @@ func (i Ident) Humanize() Ident { parts = xappend(parts, i.Parts[1:]...) } - return New(strings.Join(parts, " ")) + return Ident{Original: strings.Join(parts, " ")} } diff --git a/ident.go b/internal/flect/ident.go similarity index 65% rename from ident.go rename to internal/flect/ident.go index 9189e9a..35bdbee 100644 --- a/ident.go +++ b/internal/flect/ident.go @@ -34,25 +34,14 @@ func toParts(s string) []string { if len(s) == 0 { return parts } - if _, ok := baseAcronyms[strings.ToUpper(s)]; ok { - return []string{strings.ToUpper(s)} + up := strings.ToUpper(s) + if _, ok := BaseAcronyms[up]; ok { + return []string{up} } 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 } @@ -72,7 +61,7 @@ func toParts(s string) []string { prev = c continue } - if unicode.IsUpper(c) && baseAcronyms[strings.ToUpper(x.String())] { + if _, isAcronym := BaseAcronyms[x.String()]; unicode.IsUpper(c) && isAcronym { parts = xappend(parts, x.String()) x.Reset() x.WriteRune(c) @@ -110,13 +99,13 @@ func (i Ident) ReplaceSuffix(orig, new string) Ident { return New(strings.TrimSuffix(i.Original, orig) + new) } -//UnmarshalText unmarshalls byte array into the Ident +// 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 +// MarshalText marshals Ident into byte array func (i Ident) MarshalText() ([]byte, error) { return []byte(i.Original), nil } diff --git a/internal/flect/lower_upper.go b/internal/flect/lower_upper.go new file mode 100644 index 0000000..617e634 --- /dev/null +++ b/internal/flect/lower_upper.go @@ -0,0 +1,13 @@ +package flect + +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/ordinalize.go b/internal/flect/ordinalize.go similarity index 66% rename from ordinalize.go rename to internal/flect/ordinalize.go index 1ce27b3..f63ac6b 100644 --- a/ordinalize.go +++ b/internal/flect/ordinalize.go @@ -1,11 +1,9 @@ package flect -import ( - "fmt" - "strconv" -) +import "strconv" // Ordinalize converts a number to an ordinal version +// // 42 = 42nd // 45 = 45th // 1 = 1st @@ -14,6 +12,7 @@ func Ordinalize(s string) string { } // Ordinalize converts a number to an ordinal version +// // 42 = 42nd // 45 = 45th // 1 = 1st @@ -22,22 +21,21 @@ func (i Ident) Ordinalize() Ident { if err != nil { return i } - var s string + var suffix string switch abs(number) % 100 { case 11, 12, 13: - s = fmt.Sprintf("%dth", number) + suffix = "th" default: switch abs(number) % 10 { case 1: - s = fmt.Sprintf("%dst", number) + suffix = "st" case 2: - s = fmt.Sprintf("%dnd", number) + suffix = "nd" case 3: - s = fmt.Sprintf("%drd", number) + suffix = "rd" + default: + suffix = "th" } } - if s != "" { - return New(s) - } - return New(fmt.Sprintf("%dth", number)) + return Ident{Original: strconv.Itoa(number) + suffix} } diff --git a/pascalize.go b/internal/flect/pascalize.go similarity index 76% rename from pascalize.go rename to internal/flect/pascalize.go index 6396d0d..da2ede5 100644 --- a/pascalize.go +++ b/internal/flect/pascalize.go @@ -5,6 +5,7 @@ import ( ) // Pascalize returns a string with each segment capitalized +// // user = User // bob dylan = BobDylan // widget_id = WidgetID @@ -13,6 +14,7 @@ func Pascalize(s string) string { } // Pascalize returns a string with each segment capitalized +// // user = User // bob dylan = BobDylan // widget_id = WidgetID @@ -25,8 +27,8 @@ func (i Ident) Pascalize() Ident { return i } capLen := 1 - if _, ok := baseAcronyms[strings.ToUpper(i.Parts[0])]; ok { + 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 Ident{Original: string(strings.ToUpper(c.Original[0:capLen])) + c.Original[capLen:]} } diff --git a/internal/flect/plural_rules.go b/internal/flect/plural_rules.go new file mode 100644 index 0000000..b8d9aa4 --- /dev/null +++ b/internal/flect/plural_rules.go @@ -0,0 +1,430 @@ +package flect + +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/pluralize.go b/internal/flect/pluralize.go similarity index 83% rename from pluralize.go rename to internal/flect/pluralize.go index d0ac77d..158bf3b 100644 --- a/pluralize.go +++ b/internal/flect/pluralize.go @@ -8,6 +8,7 @@ import ( var pluralMoot = &sync.RWMutex{} // Pluralize returns a plural version of the string +// // user = users // person = people // datum = data @@ -15,7 +16,8 @@ func Pluralize(s string) string { return New(s).Pluralize().String() } -// 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 { @@ -26,6 +28,7 @@ func PluralizeWithSize(s string, i int) string { } // Pluralize returns a plural version of the string +// // user = users // person = people // datum = data @@ -36,18 +39,20 @@ func (i Ident) Pluralize() Ident { } pluralMoot.RLock() - defer pluralMoot.RUnlock() // 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 } @@ -55,10 +60,14 @@ func (i Ident) Pluralize() Ident { if s == Capitalize(s) { p = Capitalize(p) } + pluralMoot.RUnlock() return i.ReplaceSuffix(s, p) } - for _, r := range pluralRules { + rules := pluralRules + pluralMoot.RUnlock() + + for _, r := range rules { if strings.HasSuffix(s, r.suffix) { return i.ReplaceSuffix(s, r.fn(s)) } diff --git a/rule.go b/internal/flect/rule.go similarity index 100% rename from rule.go rename to internal/flect/rule.go diff --git a/singular_rules.go b/internal/flect/singular_rules.go similarity index 100% rename from singular_rules.go rename to internal/flect/singular_rules.go diff --git a/singularize.go b/internal/flect/singularize.go similarity index 82% rename from singularize.go rename to internal/flect/singularize.go index d00cf4f..9f1bfd6 100644 --- a/singularize.go +++ b/internal/flect/singularize.go @@ -8,6 +8,7 @@ import ( var singularMoot = &sync.RWMutex{} // Singularize returns a singular version of the string +// // users = user // data = datum // people = person @@ -15,7 +16,8 @@ func Singularize(s string) string { return New(s).Singularize().String() } -// 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 { @@ -23,6 +25,7 @@ func SingularizeWithSize(s string, i int) string { } // Singularize returns a singular version of the string +// // users = user // data = datum // people = person @@ -33,13 +36,14 @@ func (i Ident) Singularize() Ident { } singularMoot.RLock() - defer singularMoot.RUnlock() // 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 } @@ -48,14 +52,19 @@ func (i Ident) Singularize() Ident { if s == Capitalize(s) { p = Capitalize(p) } + singularMoot.RUnlock() return i.ReplaceSuffix(s, p) } if _, ok := singleToPlural[ls]; ok { + singularMoot.RUnlock() return i } - for _, r := range singularRules { + rules := singularRules + singularMoot.RUnlock() + + for _, r := range rules { if strings.HasSuffix(s, r.suffix) { return i.ReplaceSuffix(s, r.fn(s)) } diff --git a/titleize.go b/internal/flect/titleize.go similarity index 95% rename from titleize.go rename to internal/flect/titleize.go index 0878ada..815d307 100644 --- a/titleize.go +++ b/internal/flect/titleize.go @@ -6,6 +6,7 @@ import ( ) // 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" @@ -14,6 +15,7 @@ func Titleize(s string) 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" @@ -34,5 +36,5 @@ func (i Ident) Titleize() Ident { parts = append(parts, x) } - return New(strings.Join(parts, " ")) + return Ident{Original: strings.Join(parts, " ")} } diff --git a/underscore.go b/internal/flect/underscore.go similarity index 99% rename from underscore.go rename to internal/flect/underscore.go index d42859a..6521766 100644 --- a/underscore.go +++ b/internal/flect/underscore.go @@ -6,6 +6,7 @@ import ( ) // Underscore a string +// // bob dylan --> bob_dylan // Nice to see you! --> nice_to_see_you // widgetID --> widget_id @@ -14,6 +15,7 @@ func Underscore(s string) string { } // Underscore a string +// // bob dylan --> bob_dylan // Nice to see you! --> nice_to_see_you // widgetID --> widget_id 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 deleted file mode 100644 index 930da58..0000000 --- a/lower_upper.go +++ /dev/null @@ -1,13 +0,0 @@ -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..92e42b3 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/flect" ) // 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..b1e8fd5 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/flect" ) 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..792f433 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/flect" // 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..4b91bc5 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/flect" ) // 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_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_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 deleted file mode 100644 index 3904e79..0000000 --- a/plural_rules.go +++ /dev/null @@ -1,417 +0,0 @@ -package flect - -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"}, - - // 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) - } - } - } -} 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/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_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_test.go b/underscore_test.go index 7122e9d..ca31747 100644 --- a/underscore_test.go +++ b/underscore_test.go @@ -2,12 +2,10 @@ package flect import ( "testing" - - "github.com/stretchr/testify/require" ) func Test_Underscore(t *testing.T) { - baseAcronyms["TLC"] = true + baseAcronyms["TLC"] = struct{}{} table := []tt{ {"", ""}, @@ -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)) }) 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"