From 6149506ddf3b3b812e2aa371e29ca4c5b80982b2 Mon Sep 17 00:00:00 2001 From: Mark Bates Date: Fri, 27 Jul 2018 16:05:03 +0100 Subject: [PATCH] Initial commit --- .gitignore | 28 ++++++ acronyms.go | 148 +++++++++++++++++++++++++++ camelize.go | 36 +++++++ camelize_test.go | 29 ++++++ capitalize.go | 19 ++++ capitalize_test.go | 26 +++++ cover.out | 102 +++++++++++++++++++ flect.go | 9 ++ flect_test.go | 64 ++++++++++++ go.mod | 10 ++ go.sum | 12 +++ ident.go | 110 ++++++++++++++++++++ ident_test.go | 52 ++++++++++ pascalize.go | 17 ++++ pascalize_test.go | 29 ++++++ pluralize.go | 238 ++++++++++++++++++++++++++++++++++++++++++++ pluralize_test.go | 17 ++++ rule.go | 10 ++ singularize.go | 101 +++++++++++++++++++ singularize_test.go | 17 ++++ titleize.go | 23 +++++ titleize_test.go | 26 +++++ underscore.go | 26 +++++ underscore_test.go | 26 +++++ 24 files changed, 1175 insertions(+) create mode 100644 .gitignore create mode 100644 acronyms.go create mode 100644 camelize.go create mode 100644 camelize_test.go create mode 100644 capitalize.go create mode 100644 capitalize_test.go create mode 100644 cover.out create mode 100644 flect.go create mode 100644 flect_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 ident.go create mode 100644 ident_test.go create mode 100644 pascalize.go create mode 100644 pascalize_test.go create mode 100644 pluralize.go create mode 100644 pluralize_test.go create mode 100644 rule.go create mode 100644 singularize.go create mode 100644 singularize_test.go create mode 100644 titleize.go create mode 100644 titleize_test.go create mode 100644 underscore.go create mode 100644 underscore_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab4783b --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +*.log +.DS_Store +doc +tmp +pkg +*.gem +*.pid +coverage +coverage.data +build/* +*.pbxuser +*.mode1v3 +.svn +profile +.console_history +.sass-cache/* +.rake_tasks~ +*.log.lck +solr/ +.jhw-cache/ +jhw.* +*.sublime* +node_modules/ +dist/ +generated/ +.vendor/ +bin/* +gin-bin diff --git a/acronyms.go b/acronyms.go new file mode 100644 index 0000000..0208f59 --- /dev/null +++ b/acronyms.go @@ -0,0 +1,148 @@ +package flect + +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/camelize.go b/camelize.go new file mode 100644 index 0000000..1062179 --- /dev/null +++ b/camelize.go @@ -0,0 +1,36 @@ +package flect + +import ( + "strings" + "unicode" +) + +func Camelize(s string) string { + return New(s).Camelize() +} + +func (i Ident) Camelize() string { + 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 strings.Join(out, "") +} diff --git a/camelize_test.go b/camelize_test.go new file mode 100644 index 0000000..4f4de32 --- /dev/null +++ b/camelize_test.go @@ -0,0 +1,29 @@ +package flect + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Camelize(t *testing.T) { + table := []tt{ + {"", ""}, + {"bob dylan", "bobDylan"}, + {"widgetID", "widgetID"}, + {"widget_ID", "widgetID"}, + {"Widget_ID", "widgetID"}, + {"Nice to see you!", "niceToSeeYou"}, + {"*hello*", "hello"}, + {"i've read a book! have you?", "iveReadABookHaveYou"}, + {"This is `code` ok", "thisIsCodeOK"}, + } + + for _, tt := range table { + t.Run(tt.act, func(st *testing.T) { + r := require.New(st) + r.Equal(tt.exp, Camelize(tt.act)) + r.Equal(tt.exp, Camelize(tt.exp)) + }) + } +} diff --git a/capitalize.go b/capitalize.go new file mode 100644 index 0000000..6ff0d92 --- /dev/null +++ b/capitalize.go @@ -0,0 +1,19 @@ +package flect + +import "unicode" + +func Capitalize(s string) string { + return New(s).Capitalize() +} + +func (i Ident) Capitalize() string { + var x string + if len(i.parts) == 0 { + return "" + } + x = string(unicode.ToTitle(rune(i.original[0]))) + if len(i.original) > 1 { + x += i.original[1:] + } + return x +} diff --git a/capitalize_test.go b/capitalize_test.go new file mode 100644 index 0000000..dc7a0b3 --- /dev/null +++ b/capitalize_test.go @@ -0,0 +1,26 @@ +package flect + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Capitalize(t *testing.T) { + table := []tt{ + {"", ""}, + {"foo", "Foo"}, + {"WidgetID", "WidgetID"}, + {"widgetID", "WidgetID"}, + {"widget_ID", "Widget_ID"}, + {"widget ID", "Widget ID"}, + } + + for _, tt := range table { + t.Run(tt.act, func(st *testing.T) { + r := require.New(st) + r.Equal(tt.exp, Capitalize(tt.act)) + r.Equal(tt.exp, Capitalize(tt.exp)) + }) + } +} diff --git a/cover.out b/cover.out new file mode 100644 index 0000000..7afe43e --- /dev/null +++ b/cover.out @@ -0,0 +1,102 @@ +mode: set +github.com/gobuffalo/flect/titleize.go:8.32,10.2 1 1 +github.com/gobuffalo/flect/titleize.go:12.34,14.31 2 1 +github.com/gobuffalo/flect/titleize.go:22.2,22.33 1 1 +github.com/gobuffalo/flect/titleize.go:14.31,17.20 3 1 +github.com/gobuffalo/flect/titleize.go:20.3,20.27 1 1 +github.com/gobuffalo/flect/titleize.go:17.20,19.4 1 1 +github.com/gobuffalo/flect/underscore.go:8.34,10.2 1 1 +github.com/gobuffalo/flect/underscore.go:12.36,14.31 2 1 +github.com/gobuffalo/flect/underscore.go:25.2,25.48 1 1 +github.com/gobuffalo/flect/underscore.go:14.31,16.26 2 1 +github.com/gobuffalo/flect/underscore.go:21.3,21.14 1 1 +github.com/gobuffalo/flect/underscore.go:16.26,17.49 1 1 +github.com/gobuffalo/flect/underscore.go:17.49,19.5 1 1 +github.com/gobuffalo/flect/underscore.go:21.14,23.4 1 1 +github.com/gobuffalo/flect/camelize.go:8.32,10.2 1 1 +github.com/gobuffalo/flect/camelize.go:12.34,14.31 2 1 +github.com/gobuffalo/flect/camelize.go:35.2,35.30 1 1 +github.com/gobuffalo/flect/camelize.go:14.31,17.26 3 1 +github.com/gobuffalo/flect/camelize.go:31.3,31.14 1 1 +github.com/gobuffalo/flect/camelize.go:17.26,18.49 1 1 +github.com/gobuffalo/flect/camelize.go:18.49,19.15 1 1 +github.com/gobuffalo/flect/camelize.go:23.5,23.16 1 1 +github.com/gobuffalo/flect/camelize.go:28.5,28.19 1 1 +github.com/gobuffalo/flect/camelize.go:19.15,21.14 2 1 +github.com/gobuffalo/flect/camelize.go:23.16,26.14 3 1 +github.com/gobuffalo/flect/camelize.go:31.14,33.4 1 1 +github.com/gobuffalo/flect/capitalize.go:5.34,7.2 1 1 +github.com/gobuffalo/flect/capitalize.go:9.36,11.23 2 1 +github.com/gobuffalo/flect/capitalize.go:14.2,15.25 2 1 +github.com/gobuffalo/flect/capitalize.go:18.2,18.10 1 1 +github.com/gobuffalo/flect/capitalize.go:11.23,13.3 1 1 +github.com/gobuffalo/flect/capitalize.go:15.25,17.3 1 1 +github.com/gobuffalo/flect/ident.go:15.32,17.2 1 0 +github.com/gobuffalo/flect/ident.go:19.26,26.2 2 1 +github.com/gobuffalo/flect/ident.go:30.33,33.17 3 1 +github.com/gobuffalo/flect/ident.go:36.2,36.54 1 1 +github.com/gobuffalo/flect/ident.go:41.2,43.22 3 1 +github.com/gobuffalo/flect/ident.go:82.2,84.14 2 1 +github.com/gobuffalo/flect/ident.go:33.17,35.3 1 1 +github.com/gobuffalo/flect/ident.go:36.54,37.31 1 1 +github.com/gobuffalo/flect/ident.go:37.31,39.4 1 1 +github.com/gobuffalo/flect/ident.go:43.22,57.25 2 1 +github.com/gobuffalo/flect/ident.go:61.3,61.17 1 1 +github.com/gobuffalo/flect/ident.go:67.3,67.51 1 1 +github.com/gobuffalo/flect/ident.go:73.3,73.82 1 1 +github.com/gobuffalo/flect/ident.go:78.3,80.11 3 1 +github.com/gobuffalo/flect/ident.go:57.25,58.12 1 0 +github.com/gobuffalo/flect/ident.go:61.17,65.12 4 1 +github.com/gobuffalo/flect/ident.go:67.51,71.12 4 1 +github.com/gobuffalo/flect/ident.go:73.82,76.12 3 1 +github.com/gobuffalo/flect/ident.go:89.27,90.27 1 1 +github.com/gobuffalo/flect/ident.go:95.2,95.27 1 1 +github.com/gobuffalo/flect/ident.go:90.27,91.13 1 1 +github.com/gobuffalo/flect/ident.go:91.13,93.4 1 1 +github.com/gobuffalo/flect/ident.go:98.49,99.23 1 1 +github.com/gobuffalo/flect/ident.go:115.2,115.10 1 1 +github.com/gobuffalo/flect/ident.go:99.23,101.28 2 1 +github.com/gobuffalo/flect/ident.go:104.3,104.55 1 1 +github.com/gobuffalo/flect/ident.go:111.3,111.14 1 1 +github.com/gobuffalo/flect/ident.go:101.28,103.4 1 1 +github.com/gobuffalo/flect/ident.go:104.55,105.32 1 1 +github.com/gobuffalo/flect/ident.go:108.4,109.9 2 1 +github.com/gobuffalo/flect/ident.go:105.32,106.13 1 1 +github.com/gobuffalo/flect/ident.go:111.14,113.4 1 1 +github.com/gobuffalo/flect/pascalize.go:7.33,9.2 1 1 +github.com/gobuffalo/flect/pascalize.go:11.35,13.17 2 1 +github.com/gobuffalo/flect/pascalize.go:16.2,16.52 1 1 +github.com/gobuffalo/flect/pascalize.go:13.17,15.3 1 1 +github.com/gobuffalo/flect/pluralize.go:8.33,10.2 1 1 +github.com/gobuffalo/flect/pluralize.go:13.32,15.2 1 1 +github.com/gobuffalo/flect/pluralize.go:18.31,19.16 1 1 +github.com/gobuffalo/flect/pluralize.go:25.2,26.18 2 0 +github.com/gobuffalo/flect/pluralize.go:19.16,21.23 2 1 +github.com/gobuffalo/flect/pluralize.go:21.23,23.4 1 1 +github.com/gobuffalo/flect/pluralize.go:29.32,31.2 1 0 +github.com/gobuffalo/flect/pluralize.go:34.31,35.16 1 1 +github.com/gobuffalo/flect/pluralize.go:41.2,42.18 2 1 +github.com/gobuffalo/flect/pluralize.go:35.16,37.23 2 1 +github.com/gobuffalo/flect/pluralize.go:37.23,39.4 1 1 +github.com/gobuffalo/flect/pluralize.go:46.32,49.2 2 1 +github.com/gobuffalo/flect/pluralize.go:52.32,55.2 2 0 +github.com/gobuffalo/flect/pluralize.go:58.32,61.2 2 1 +github.com/gobuffalo/flect/pluralize.go:63.27,64.11 1 1 +github.com/gobuffalo/flect/pluralize.go:68.2,68.14 1 1 +github.com/gobuffalo/flect/pluralize.go:65.31,66.14 1 1 +github.com/gobuffalo/flect/pluralize.go:71.32,74.2 2 1 +github.com/gobuffalo/flect/pluralize.go:81.28,81.40 1 1 +github.com/gobuffalo/flect/pluralize.go:105.35,106.23 1 1 +github.com/gobuffalo/flect/pluralize.go:109.2,111.39 3 1 +github.com/gobuffalo/flect/pluralize.go:114.2,115.39 2 1 +github.com/gobuffalo/flect/pluralize.go:120.2,120.32 1 1 +github.com/gobuffalo/flect/pluralize.go:128.2,128.35 1 1 +github.com/gobuffalo/flect/pluralize.go:131.2,132.33 2 1 +github.com/gobuffalo/flect/pluralize.go:106.23,108.3 1 1 +github.com/gobuffalo/flect/pluralize.go:111.39,113.3 1 1 +github.com/gobuffalo/flect/pluralize.go:115.39,118.3 2 1 +github.com/gobuffalo/flect/pluralize.go:120.32,121.40 1 1 +github.com/gobuffalo/flect/pluralize.go:121.40,124.4 2 1 +github.com/gobuffalo/flect/pluralize.go:128.35,130.3 1 1 +github.com/gobuffalo/flect/pluralize.go:254.13,255.35 1 1 +github.com/gobuffalo/flect/pluralize.go:255.35,257.3 1 1 diff --git a/flect.go b/flect.go new file mode 100644 index 0000000..14df57f --- /dev/null +++ b/flect.go @@ -0,0 +1,9 @@ +package flect + +func isVowel(r rune) bool { + switch r { + case 'a', 'e', 'i', 'o', 'u': + return true + } + return false +} diff --git a/flect_test.go b/flect_test.go new file mode 100644 index 0000000..c9f8721 --- /dev/null +++ b/flect_test.go @@ -0,0 +1,64 @@ +package flect + +type tt struct { + act string + exp string +} + +var singlePluralAssertions = []tt{ + {"", ""}, + {"user", "users"}, + {"cat", "cats"}, + {"truss", "trusses"}, + {"bus", "busses"}, + {"marsh", "marshes"}, + {"lunch", "lunches"}, + {"tax", "taxes"}, + {"blitz", "blitzes"}, + {"fez", "fezzes"}, + {"wolf", "wolves"}, + {"roof", "roofs"}, + {"belief", "beliefs"}, + {"chef", "chefs"}, + {"chief", "chiefs"}, + {"city", "cities"}, + {"puppy", "puppies"}, + {"ray", "rays"}, + {"boy", "boys"}, + {"potato", "potatoes"}, + {"tomato", "tomatoes"}, + {"photo", "photos"}, + {"piano", "pianos"}, + {"halo", "halos"}, + {"cactus", "cacti"}, + {"focus", "foci"}, + {"datum", "data"}, + {"analysis", "analyses"}, + {"ellipsis", "ellipses"}, + {"phenomenon", "phenomena"}, + {"criterion", "criteria"}, + {"sheep", "sheep"}, + {"series", "series"}, + {"species", "species"}, + {"dear", "dear"}, + {"child", "children"}, + {"goose", "geese"}, + {"man", "men"}, + {"woman", "women"}, + {"tooth", "teeth"}, + {"foot", "feet"}, + {"mouse", "mice"}, + {"person", "people"}, +} + +var pluralSingularAssertions = []tt{} + +func init() { + for k, v := range singleToPlural { + singlePluralAssertions = append(singlePluralAssertions, tt{k, v}) + } + + for _, a := range singlePluralAssertions { + pluralSingularAssertions = append(pluralSingularAssertions, tt{act: a.exp, exp: a.act}) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..136bea2 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/gobuffalo/flect + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/jtolds/gls v4.2.1+incompatible // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/smartystreets/assertions v0.0.0-20180725160413-e900ae048470 // indirect + github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect + github.com/stretchr/testify v1.2.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..15b5323 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +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/smartystreets/assertions v0.0.0-20180725160413-e900ae048470 h1:R0uuDVEvfDha2O6dfJRr4/5NBHKEbZhMPZmqOWpEkSo= +github.com/smartystreets/assertions v0.0.0-20180725160413-e900ae048470/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo= +github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/ident.go b/ident.go new file mode 100644 index 0000000..117f6ba --- /dev/null +++ b/ident.go @@ -0,0 +1,110 @@ +package flect + +import ( + "regexp" + "strings" + "unicode" + "unicode/utf8" +) + +type Ident struct { + original string + parts []string +} + +func (i Ident) String() string { + return i.original +} + +func New(s string) Ident { + i := Ident{ + original: s, + parts: toParts(s), + } + + return i +} + +var splitRx = regexp.MustCompile("[^\\p{L}]") + +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 string + for _, c := range s { + cs := string(c) + // 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) + x = cs + prev = c + continue + } + if unicode.IsUpper(c) && !unicode.IsUpper(prev) { + parts = xappend(parts, x) + x = cs + prev = c + continue + } + if unicode.IsLetter(c) || unicode.IsDigit(c) || unicode.IsPunct(c) || c == '`' { + prev = c + x += cs + continue + } + parts = xappend(parts, x) + x = "" + prev = c + } + parts = xappend(parts, x) + + return parts +} + +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 +} diff --git a/ident_test.go b/ident_test.go new file mode 100644 index 0000000..ad8a76d --- /dev/null +++ b/ident_test.go @@ -0,0 +1,52 @@ +package flect + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_New(t *testing.T) { + table := []Ident{ + {"", []string{}}, + {"widget", []string{"widget"}}, + {"widget_id", []string{"widget", "ID"}}, + {"WidgetID", []string{"Widget", "ID"}}, + {"Widget_ID", []string{"Widget", "ID"}}, + {"widget_ID", []string{"widget", "ID"}}, + {"widget/ID", []string{"widget", "ID"}}, + {"widgetID", []string{"widget", "ID"}}, + {"widgetName", []string{"widget", "Name"}}, + {"sql", []string{"SQL"}}, + {"sQl", []string{"SQL"}}, + {"id", []string{"ID"}}, + {"Id", []string{"ID"}}, + {"iD", []string{"ID"}}, + {"html", []string{"HTML"}}, + {"Html", []string{"HTML"}}, + {"HTML", []string{"HTML"}}, + {"with `code` inside", []string{"with", "`code`", "inside"}}, + {"Donald E. Knuth", []string{"Donald", "E.", "Knuth"}}, + {"Random text with *(bad)* characters", []string{"Random", "text", "with", "*(bad)*", "characters"}}, + {"Allow_Under_Scores", []string{"Allow", "Under", "Scores"}}, + {"Trailing bad characters!@#", []string{"Trailing", "bad", "characters!@#"}}, + {"!@#Leading bad characters", []string{"!@#", "Leading", "bad", "characters"}}, + {"Squeeze separators", []string{"Squeeze", "separators"}}, + {"Test with + sign", []string{"Test", "with", "sign"}}, + {"Malmö", []string{"Malmö"}}, + {"Garçons", []string{"Garçons"}}, + {"Opsů", []string{"Opsů"}}, + {"Ærøskøbing", []string{"Ærøskøbing"}}, + {"Aßlar", []string{"Aßlar"}}, + {"Japanese: 日本語", []string{"Japanese", "日本語"}}, + } + + for _, tt := range table { + t.Run(tt.original, func(st *testing.T) { + r := require.New(st) + i := New(tt.original) + r.Equal(tt.original, i.original) + r.Equal(tt.parts, i.parts) + }) + } +} diff --git a/pascalize.go b/pascalize.go new file mode 100644 index 0000000..e3f18c2 --- /dev/null +++ b/pascalize.go @@ -0,0 +1,17 @@ +package flect + +import ( + "unicode" +) + +func Pascalize(s string) string { + return New(s).Pascalize() +} + +func (i Ident) Pascalize() string { + c := i.Camelize() + if len(c) == 0 { + return c + } + return string(unicode.ToUpper(rune(c[0]))) + c[1:] +} diff --git a/pascalize_test.go b/pascalize_test.go new file mode 100644 index 0000000..1e06747 --- /dev/null +++ b/pascalize_test.go @@ -0,0 +1,29 @@ +package flect + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Pascalize(t *testing.T) { + table := []tt{ + {"", ""}, + {"bob dylan", "BobDylan"}, + {"widgetID", "WidgetID"}, + {"widget_ID", "WidgetID"}, + {"Widget_ID", "WidgetID"}, + {"Nice to see you!", "NiceToSeeYou"}, + {"*hello*", "Hello"}, + {"i've read a book! have you?", "IveReadABookHaveYou"}, + {"This is `code` ok", "ThisIsCodeOK"}, + } + + for _, tt := range table { + t.Run(tt.act, func(st *testing.T) { + r := require.New(st) + r.Equal(tt.exp, Pascalize(tt.act)) + r.Equal(tt.exp, Pascalize(tt.exp)) + }) + } +} diff --git a/pluralize.go b/pluralize.go new file mode 100644 index 0000000..f85f272 --- /dev/null +++ b/pluralize.go @@ -0,0 +1,238 @@ +package flect + +// source for grammar rules: https://www.grammarly.com/blog/plural-nouns/ +import ( + "strings" +) + +func Pluralize(s string) string { + return New(s).Pluralize() +} + +func (i Ident) Pluralize() string { + if len(i.parts) == 0 { + return "" + } + li := len(i.parts) - 1 + last := i.parts[li] + if _, ok := pluralToSingle[strings.ToLower(last)]; ok { + return i.original + } + parts := i.parts[:li] + if p, ok := singleToPlural[strings.ToLower(last)]; ok { + parts = append(parts, p) + return strings.Join(parts, " ") + } + + for _, r := range pluralRules { + if strings.HasSuffix(last, r.suffix) { + parts = append(parts, r.fn(last)) + return strings.Join(parts, " ") + } + } + + //To make regular nouns plural, add ‑s to the end. + if !strings.HasSuffix(last, "s") { + last += "s" + } + parts = append(parts, last) + return strings.Join(parts, " ") +} + +// If the singular noun ends in ‑s, -ss, -sh, -ch, -x, or -z, add ‑es to the end to make it plural. +func esPlural(s string) string { + return s + "es" +} + +// If the noun ends with ‑f or ‑fe, the f is often changed to ‑ve before adding the -s to form the plural version. +func fPlural(s string) string { + if len(s) > 1 { + c := s[len(s)-2] + if isVowel(rune(c)) { + return s + "s" + } + } + s = strings.TrimSuffix(s, "f") + return s + "ves" +} + +func fePlural(s string) string { + return fPlural(strings.TrimSuffix(s, "e")) +} + +// If a singular noun ends in ‑y and the letter before the -y is a consonant, change the ending to ‑ies to make the noun plural. +func yPlural(s string) string { + if len(s) > 1 { + c := s[len(s)-2] + if isVowel(rune(c)) { + return s + "s" + } + } + s = strings.TrimSuffix(s, "y") + return s + "ies" +} + +// If the singular noun ends in ‑us, the plural ending is frequently ‑i. +func usPlural(s string) string { + s = strings.TrimSuffix(s, "us") + return s + "i" +} + +// If the singular noun ends in ‑is, the plural ending is ‑es. +func isPlural(s string) string { + s = strings.TrimSuffix(s, "is") + return s + "es" +} + +// If the singular noun ends in ‑on, the plural ending is ‑a. +func onPlural(s string) string { + s = strings.TrimSuffix(s, "on") + return s + "a" +} + +func umPlural(s string) string { + s = strings.TrimSuffix(s, "um") + return s + "a" +} + +var pluralRules = []rule{ + {"es", noop}, + {"ves", noop}, + {"ies", noop}, + {"i", noop}, + {"es", noop}, + {"a", noop}, + {"ss", esPlural}, + {"sh", esPlural}, + {"ch", esPlural}, + {"x", esPlural}, + {"z", esPlural}, + {"o", esPlural}, + {"f", fPlural}, + {"fe", fPlural}, + {"y", yPlural}, + {"us", usPlural}, + {"is", isPlural}, + {"on", onPlural}, + {"um", umPlural}, +} + +var singleToPlural = map[string]string{ + "matrix": "matrices", + "vertix": "vertices", + "index": "indices", + "mouse": "mice", + "louse": "lice", + "ress": "resses", + "ox": "oxen", + "quiz": "quizzes", + "series": "series", + "octopus": "octopi", + "person": "people", + "man": "men", + "child": "children", + "equipment": "equipment", + "information": "information", + "rice": "rice", + "money": "money", + "species": "species", + "fish": "fish", + "sheep": "sheep", + "jeans": "jeans", + "police": "police", + "dear": "dear", + "goose": "geese", + "woman": "women", + "tooth": "teeth", + "foot": "feet", + "bus": "busses", + "fez": "fezzes", + "piano": "pianos", + "halo": "halos", + "photo": "photos", + "aircraft": "aircraft", + "alumna": "alumnae", + "alumnus": "alumni", + "analysis": "analyses", + "antenna": "antennas", + "antithesis": "antitheses", + "apex": "apexes", + "appendix": "appendices", + "axis": "axes", + "bacillus": "bacilli", + "bacterium": "bacteria", + "basis": "bases", + "beau": "beaus", + "bison": "bison", + "bureau": "bureaus", + "château": "châteaux", + "codex": "codices", + "concerto": "concertos", + "corpus": "corpora", + "crisis": "crises", + "curriculum": "curriculums", + "deer": "deer", + "diagnosis": "diagnoses", + "die": "dice", + "dwarf": "dwarves", + "ellipsis": "ellipses", + "erratum": "errata", + "faux pas": "faux pas", + "focus": "foci", + "formula": "formulas", + "fungus": "fungi", + "genus": "genera", + "graffito": "graffiti", + "grouse": "grouse", + "half": "halves", + "hoof": "hooves", + "hypothesis": "hypotheses", + "larva": "larvae", + "libretto": "librettos", + "loaf": "loaves", + "locus": "loci", + "medium": "mediums", + "memorandum": "memoranda", + "minutia": "minutiae", + "moose": "moose", + "nebula": "nebulae", + "nucleus": "nuclei", + "oasis": "oases", + "offspring": "offspring", + "opus": "opera", + "ovum": "ova", + "parenthesis": "parentheses", + "phenomenon": "phenomena", + "phylum": "phyla", + "prognosis": "prognoses", + "radius": "radiuses", + "referendum": "referendums", + "salmon": "salmon", + "shrimp": "shrimp", + "stimulus": "stimuli", + "stratum": "strata", + "swine": "swine", + "syllabus": "syllabi", + "symposium": "symposiums", + "synopsis": "synopses", + "tableau": "tableaus", + "thesis": "theses", + "thief": "thieves", + "trout": "trout", + "tuna": "tuna", + "vertebra": "vertebrae", + "vita": "vitae", + "vortex": "vortices", + "wharf": "wharves", + "wife": "wives", + "wolf": "wolves", + "datum": "data", +} + +var pluralToSingle = map[string]string{} + +func init() { + for k, v := range singleToPlural { + pluralToSingle[v] = k + } +} diff --git a/pluralize_test.go b/pluralize_test.go new file mode 100644 index 0000000..74a56e7 --- /dev/null +++ b/pluralize_test.go @@ -0,0 +1,17 @@ +package flect + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Pluralize(t *testing.T) { + for _, tt := range singlePluralAssertions { + t.Run(tt.act, func(st *testing.T) { + r := require.New(st) + r.Equal(tt.exp, Pluralize(tt.act)) + r.Equal(tt.exp, Pluralize(tt.exp)) + }) + } +} diff --git a/rule.go b/rule.go new file mode 100644 index 0000000..dc616b3 --- /dev/null +++ b/rule.go @@ -0,0 +1,10 @@ +package flect + +type ruleFn func(string) string + +type rule struct { + suffix string + fn ruleFn +} + +func noop(s string) string { return s } diff --git a/singularize.go b/singularize.go new file mode 100644 index 0000000..f028ffa --- /dev/null +++ b/singularize.go @@ -0,0 +1,101 @@ +package flect + +// source for grammar rules: https://www.grammarly.com/blog/plural-nouns/ +import ( + "strings" +) + +func Singularize(s string) string { + return New(s).Singularize() +} + +func (i Ident) Singularize() string { + if len(i.parts) == 0 { + return "" + } + li := len(i.parts) - 1 + last := i.parts[li] + if _, ok := singleToPlural[strings.ToLower(last)]; ok { + return i.original + } + parts := i.parts[:li] + if p, ok := pluralToSingle[strings.ToLower(last)]; ok { + parts = append(parts, p) + return strings.Join(parts, " ") + } + + for _, r := range singularRules { + if strings.HasSuffix(last, r.suffix) { + parts = append(parts, r.fn(last)) + return strings.Join(parts, " ") + } + } + + if len(last) > 2 { + if isVowel(rune(last[len(last)-2])) { + parts = append(parts, last) + return strings.Join(parts, " ") + } + } + //To make regular nouns plural, add ‑s to the end. + parts = append(parts, strings.TrimSuffix(last, "s")) + return strings.Join(parts, " ") +} + +// If the singular noun ends in ‑s, -ss, -sh, -ch, -x, or -z, add ‑es to the end to make it plural. +func esSingular(s string) string { + return strings.TrimSuffix(s, "es") +} + +// If the noun ends with ‑f or ‑fe, the f is often changed to ‑ve before adding the -s to form the plural version. +func vesSingular(s string) string { + s = strings.TrimSuffix(s, "ves") + return s + "f" +} + +// If a singular noun ends in ‑y and the letter before the -y is a consonant, change the ending to ‑ies to make the noun plural. +func iesSingular(s string) string { + s = strings.TrimSuffix(s, "ies") + return s + "y" +} + +// If the singular noun ends in ‑us, the plural ending is frequently ‑i. +func iSingular(s string) string { + s = strings.TrimSuffix(s, "i") + return s + "us" +} + +// If the singular noun ends in ‑on, the plural ending is ‑a. +func aSingular(s string) string { + s = strings.TrimSuffix(s, "a") + return s + "on" +} + +func exSingular(s string) string { + s = strings.TrimSuffix(s, "ex") + return s + "ix" +} + +func ssSingular(s string) string { + if len(s) > 3 { + if isVowel(rune(s[len(s)-3])) { + return s + } + } + return strings.TrimSuffix(s, "ss") +} + +var singularRules = []rule{ + {"ix", noop}, + {"us", noop}, + {"y", noop}, + {"on", noop}, + {"f", noop}, + {"i", iSingular}, + {"ss", ssSingular}, + {"ies", iesSingular}, + {"ves", vesSingular}, + {"ex", exSingular}, + {"a", aSingular}, + {"es", esSingular}, +} diff --git a/singularize_test.go b/singularize_test.go new file mode 100644 index 0000000..68901ba --- /dev/null +++ b/singularize_test.go @@ -0,0 +1,17 @@ +package flect + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Singularize(t *testing.T) { + for _, tt := range pluralSingularAssertions { + t.Run(tt.exp, func(st *testing.T) { + r := require.New(st) + r.Equal(tt.exp, Singularize(tt.act)) + r.Equal(tt.exp, Singularize(tt.exp)) + }) + } +} diff --git a/titleize.go b/titleize.go new file mode 100644 index 0000000..c83a0f5 --- /dev/null +++ b/titleize.go @@ -0,0 +1,23 @@ +package flect + +import ( + "strings" + "unicode" +) + +func Titleize(s string) string { + return New(s).Titleize() +} + +func (i Ident) Titleize() string { + var parts []string + for _, part := range i.parts { + var x string + x = string(unicode.ToTitle(rune(part[0]))) + if len(part) > 1 { + x += part[1:] + } + parts = append(parts, x) + } + return strings.Join(parts, " ") +} diff --git a/titleize_test.go b/titleize_test.go new file mode 100644 index 0000000..dd833b8 --- /dev/null +++ b/titleize_test.go @@ -0,0 +1,26 @@ +package flect + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Titleize(t *testing.T) { + table := []tt{ + {"", ""}, + {"bob dylan", "Bob Dylan"}, + {"Nice to see you!", "Nice To See You!"}, + {"*hello*", "*hello*"}, + {"i've read a book! have you?", "I've Read A Book! Have You?"}, + {"This is `code` ok", "This Is `code` OK"}, + } + + for _, tt := range table { + t.Run(tt.act, func(st *testing.T) { + r := require.New(st) + r.Equal(tt.exp, Titleize(tt.act)) + r.Equal(tt.exp, Titleize(tt.exp)) + }) + } +} diff --git a/underscore.go b/underscore.go new file mode 100644 index 0000000..83ed6dd --- /dev/null +++ b/underscore.go @@ -0,0 +1,26 @@ +package flect + +import ( + "strings" + "unicode" +) + +func Underscore(s string) string { + return New(s).Underscore() +} + +func (i Ident) Underscore() string { + var out []string + for _, part := range i.parts { + var x string + for _, c := range part { + if unicode.IsLetter(c) || unicode.IsDigit(c) { + x += string(c) + } + } + if x != "" { + out = append(out, x) + } + } + return strings.ToLower(strings.Join(out, "_")) +} diff --git a/underscore_test.go b/underscore_test.go new file mode 100644 index 0000000..3a85380 --- /dev/null +++ b/underscore_test.go @@ -0,0 +1,26 @@ +package flect + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Underscore(t *testing.T) { + table := []tt{ + {"", ""}, + {"bob dylan", "bob_dylan"}, + {"Nice to see you!", "nice_to_see_you"}, + {"*hello*", "hello"}, + {"i've read a book! have you?", "ive_read_a_book_have_you"}, + {"This is `code` ok", "this_is_code_ok"}, + } + + for _, tt := range table { + t.Run(tt.act, func(st *testing.T) { + r := require.New(st) + r.Equal(tt.exp, Underscore(tt.act)) + r.Equal(tt.exp, Underscore(tt.exp)) + }) + } +}