├── .github ├── FUNDING.yml └── workflows │ ├── standard-go-test.yml │ └── standard-stale.yml ├── .gitignore ├── .gometalinter.json ├── LICENSE ├── Makefile ├── README.md ├── SHOULDERS.md ├── acronyms.go ├── camelize.go ├── camelize_test.go ├── capitalize.go ├── capitalize_test.go ├── custom_data.go ├── dasherize.go ├── dasherize_test.go ├── flect.go ├── flect_test.go ├── go.mod ├── go.sum ├── humanize.go ├── humanize_test.go ├── ident.go ├── ident_test.go ├── lower_upper.go ├── lower_upper_test.go ├── name ├── char.go ├── char_test.go ├── file.go ├── file_test.go ├── folder.go ├── folder_test.go ├── ident.go ├── interface.go ├── interface_test.go ├── join.go ├── join_test.go ├── key.go ├── key_test.go ├── name.go ├── name_test.go ├── os_path.go ├── os_path_test.go ├── package.go ├── package_test.go ├── param_id.go ├── param_id_test.go ├── resource.go ├── resource_test.go ├── tablize.go ├── tablize_test.go ├── url.go ├── url_test.go ├── var_case.go └── var_case_test.go ├── ordinalize.go ├── ordinalize_test.go ├── pascalize.go ├── pascalize_test.go ├── plural_rules.go ├── pluralize.go ├── pluralize_test.go ├── rule.go ├── singular_rules.go ├── singularize.go ├── singularize_test.go ├── titleize.go ├── titleize_test.go ├── underscore.go ├── underscore_test.go └── version.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: markbates 4 | patreon: buffalo 5 | -------------------------------------------------------------------------------- /.github/workflows/standard-go-test.yml: -------------------------------------------------------------------------------- 1 | name: Standard Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | jobs: 9 | call-standard-test: 10 | name: Test 11 | uses: gobuffalo/.github/.github/workflows/go-test.yml@v1 12 | secrets: inherit 13 | -------------------------------------------------------------------------------- /.github/workflows/standard-stale.yml: -------------------------------------------------------------------------------- 1 | name: Standard Autocloser 2 | 3 | on: 4 | schedule: 5 | - cron: "30 1 * * *" 6 | 7 | jobs: 8 | call-standard-autocloser: 9 | name: Autocloser 10 | uses: gobuffalo/.github/.github/workflows/stale.yml@v1 11 | secrets: inherit 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | doc 4 | tmp 5 | pkg 6 | *.gem 7 | *.pid 8 | coverage 9 | coverage.data 10 | build/* 11 | *.pbxuser 12 | *.mode1v3 13 | .svn 14 | profile 15 | .console_history 16 | .sass-cache/* 17 | .rake_tasks~ 18 | *.log.lck 19 | solr/ 20 | .jhw-cache/ 21 | jhw.* 22 | *.sublime* 23 | node_modules/ 24 | dist/ 25 | generated/ 26 | .vendor/ 27 | bin/* 28 | gin-bin 29 | .idea/ 30 | -------------------------------------------------------------------------------- /.gometalinter.json: -------------------------------------------------------------------------------- 1 | { 2 | "Enable": ["vet", "golint", "goimports", "deadcode", "gotype", "ineffassign", "misspell", "nakedret", "unconvert", "megacheck", "varcheck"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Mark Bates 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TAGS ?= "" 2 | GO_BIN ?= "go" 3 | 4 | install: 5 | $(GO_BIN) install -tags ${TAGS} -v . 6 | make tidy 7 | 8 | tidy: 9 | ifeq ($(GO111MODULE),on) 10 | $(GO_BIN) mod tidy 11 | else 12 | echo skipping go mod tidy 13 | endif 14 | 15 | deps: 16 | $(GO_BIN) get -tags ${TAGS} -t ./... 17 | make tidy 18 | 19 | build: 20 | $(GO_BIN) build -v . 21 | make tidy 22 | 23 | test: 24 | $(GO_BIN) test -cover -tags ${TAGS} ./... 25 | make tidy 26 | 27 | ci-deps: 28 | $(GO_BIN) get -tags ${TAGS} -t ./... 29 | 30 | ci-test: 31 | $(GO_BIN) test -tags ${TAGS} -race ./... 32 | 33 | lint: 34 | go get github.com/golangci/golangci-lint/cmd/golangci-lint 35 | golangci-lint run --enable-all 36 | make tidy 37 | 38 | update: 39 | ifeq ($(GO111MODULE),on) 40 | rm go.* 41 | $(GO_BIN) mod init 42 | $(GO_BIN) mod tidy 43 | else 44 | $(GO_BIN) get -u -tags ${TAGS} 45 | endif 46 | make test 47 | make install 48 | make tidy 49 | 50 | release-test: 51 | $(GO_BIN) test -tags ${TAGS} -race ./... 52 | make tidy 53 | 54 | release: 55 | $(GO_BIN) get github.com/gobuffalo/release 56 | make tidy 57 | release -y -f version.go --skip-packr 58 | make tidy 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flect 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/gobuffalo/flect.svg)](https://pkg.go.dev/github.com/gobuffalo/flect) 4 | [![Standard Test](https://github.com/gobuffalo/flect/actions/workflows/standard-go-test.yml/badge.svg)](https://github.com/gobuffalo/flect/actions/workflows/standard-go-test.yml) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/gobuffalo/flect)](https://goreportcard.com/report/github.com/gobuffalo/flect) 6 | 7 | This is a new inflection engine to replace [https://github.com/markbates/inflect](https://github.com/markbates/inflect) designed to be more modular, more readable, and easier to fix issues on than the original. 8 | 9 | Flect provides word inflection features such as `Singularize` and `Pluralize` 10 | for English nouns and text utility features such as `Camelize`, `Capitalize`, 11 | `Humanize`, and more. 12 | 13 | Due to the flexibly-complex nature of English noun inflection, it is almost 14 | impossible to cover all exceptions (such as identical/irregular plural). 15 | With this reason along with the main purpose of Flect, which is to make it 16 | easy to develop web application in Go, Flect has limitations with its own 17 | rules. 18 | 19 | * It covers regular rule (adding -s or -es and of the word) 20 | * It covers well-known irregular rules (such as -is to -es, -f to -ves, etc) 21 | * https://en.wiktionary.org/wiki/Appendix:English_irregular_nouns#Rules 22 | * It covers well-known irregular words (such as children, men, etc) 23 | * If a word can be countable and uncountable like milk or time, it will be 24 | treated as countable. 25 | * If a word has more than one plural forms, which means it has at least one 26 | irregular plural, we tried to find most popular one. (The selected plural 27 | could be odd to you, please feel free to open an issue with back data) 28 | * For example, we selected "stadiums" over "stadia", "dwarfs" over "dwarves" 29 | * One or combination of en.wiktionary.org, britannica.com, and 30 | trends.google.com are used to check the recent usage trends. 31 | * However, we cannot cover all cases and some of our cases could not fit with 32 | your situation. You can override the default with functions such as 33 | `InsertPlural()`, `InsertSingular()`, or `LoadInfrections()`. 34 | * If you have a json file named `inflections.json` in your application root, 35 | the file will be automatically loaded as your custom inflection dictionary. 36 | 37 | ## Installation 38 | 39 | ```console 40 | $ go get github.com/gobuffalo/flect 41 | ``` 42 | 43 | 44 | ## Packages 45 | 46 | ### `github.com/gobuffalo/flect` 47 | 48 | The `github.com/gobuffalo/flect` package contains "basic" inflection tools, like pluralization, singularization, etc... 49 | 50 | #### The `Ident` Type 51 | 52 | In addition to helpful methods that take in a `string` and return a `string`, there is an `Ident` type that can be used to create new, custom, inflection rules. 53 | 54 | The `Ident` type contains two fields. 55 | 56 | * `Original` - This is the original `string` that was used to create the `Ident` 57 | * `Parts` - This is a `[]string` that represents all of the "parts" of the string, that have been split apart, making the segments easier to work with 58 | 59 | Examples of creating new inflection rules using `Ident` can be found in the `github.com/gobuffalo/flect/name` package. 60 | 61 | ### `github.com/gobuffalo/flect/name` 62 | 63 | The `github.com/gobuffalo/flect/name` package contains more "business" inflection rules like creating proper names, table names, etc... 64 | -------------------------------------------------------------------------------- /SHOULDERS.md: -------------------------------------------------------------------------------- 1 | # Flect Stands on the Shoulders of Giants 2 | 3 | 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. 4 | 5 | Thank you to the following **GIANTS**: 6 | 7 | * [github.com/davecgh/go-spew](https://godoc.org/github.com/davecgh/go-spew) 8 | * [github.com/pmezard/go-difflib](https://godoc.org/github.com/pmezard/go-difflib) 9 | * [github.com/stretchr/objx](https://godoc.org/github.com/stretchr/objx) 10 | * [github.com/stretchr/testify](https://godoc.org/github.com/stretchr/testify) 11 | * [gopkg.in/check.v1](https://godoc.org/gopkg.in/check.v1) 12 | * [gopkg.in/yaml.v3](https://godoc.org/gopkg.in/yaml.v3) 13 | -------------------------------------------------------------------------------- /acronyms.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import "sync" 4 | 5 | var acronymsMoot = &sync.RWMutex{} 6 | 7 | var baseAcronyms = map[string]bool{ 8 | "OK": true, 9 | "UTF8": true, 10 | "HTML": true, 11 | "JSON": true, 12 | "JWT": true, 13 | "ID": true, 14 | "UUID": true, 15 | "SQL": true, 16 | "ACK": true, 17 | "ACL": true, 18 | "ADSL": true, 19 | "AES": true, 20 | "ANSI": true, 21 | "API": true, 22 | "ARP": true, 23 | "ATM": true, 24 | "BGP": true, 25 | "BSS": true, 26 | "CCITT": true, 27 | "CHAP": true, 28 | "CIDR": true, 29 | "CIR": true, 30 | "CLI": true, 31 | "CPE": true, 32 | "CPU": true, 33 | "CRC": true, 34 | "CRT": true, 35 | "CSMA": true, 36 | "CMOS": true, 37 | "DCE": true, 38 | "DEC": true, 39 | "DES": true, 40 | "DHCP": true, 41 | "DNS": true, 42 | "DRAM": true, 43 | "DSL": true, 44 | "DSLAM": true, 45 | "DTE": true, 46 | "DMI": true, 47 | "EHA": true, 48 | "EIA": true, 49 | "EIGRP": true, 50 | "EOF": true, 51 | "ESS": true, 52 | "FCC": true, 53 | "FCS": true, 54 | "FDDI": true, 55 | "FTP": true, 56 | "GBIC": true, 57 | "gbps": true, 58 | "GEPOF": true, 59 | "HDLC": true, 60 | "HTTP": true, 61 | "HTTPS": true, 62 | "IANA": true, 63 | "ICMP": true, 64 | "IDF": true, 65 | "IDS": true, 66 | "IEEE": true, 67 | "IETF": true, 68 | "IMAP": true, 69 | "IP": true, 70 | "IPS": true, 71 | "ISDN": true, 72 | "ISP": true, 73 | "kbps": true, 74 | "LACP": true, 75 | "LAN": true, 76 | "LAPB": true, 77 | "LAPF": true, 78 | "LLC": true, 79 | "MAC": true, 80 | "Mbps": true, 81 | "MC": true, 82 | "MDF": true, 83 | "MIB": true, 84 | "MoCA": true, 85 | "MPLS": true, 86 | "MTU": true, 87 | "NAC": true, 88 | "NAT": true, 89 | "NBMA": true, 90 | "NIC": true, 91 | "NRZ": true, 92 | "NRZI": true, 93 | "NVRAM": true, 94 | "OSI": true, 95 | "OSPF": true, 96 | "OUI": true, 97 | "PAP": true, 98 | "PAT": true, 99 | "PC": true, 100 | "PIM": true, 101 | "PCM": true, 102 | "PDU": true, 103 | "POP3": true, 104 | "POTS": true, 105 | "PPP": true, 106 | "PPTP": true, 107 | "PTT": true, 108 | "PVST": true, 109 | "RAM": true, 110 | "RARP": true, 111 | "RFC": true, 112 | "RIP": true, 113 | "RLL": true, 114 | "ROM": true, 115 | "RSTP": true, 116 | "RTP": true, 117 | "RCP": true, 118 | "SDLC": true, 119 | "SFD": true, 120 | "SFP": true, 121 | "SLARP": true, 122 | "SLIP": true, 123 | "SMTP": true, 124 | "SNA": true, 125 | "SNAP": true, 126 | "SNMP": true, 127 | "SOF": true, 128 | "SRAM": true, 129 | "SSH": true, 130 | "SSID": true, 131 | "STP": true, 132 | "SYN": true, 133 | "TDM": true, 134 | "TFTP": true, 135 | "TIA": true, 136 | "TOFU": true, 137 | "UDP": true, 138 | "URL": true, 139 | "URI": true, 140 | "USB": true, 141 | "UTP": true, 142 | "VC": true, 143 | "VLAN": true, 144 | "VLSM": true, 145 | "VPN": true, 146 | "W3C": true, 147 | "WAN": true, 148 | "WEP": true, 149 | "WiFi": true, 150 | "WPA": true, 151 | "WWW": true, 152 | } 153 | -------------------------------------------------------------------------------- /camelize.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | ) 7 | 8 | // Camelize returns a camelize version of a string 9 | // bob dylan = bobDylan 10 | // widget_id = widgetID 11 | // WidgetID = widgetID 12 | func Camelize(s string) string { 13 | return New(s).Camelize().String() 14 | } 15 | 16 | // Camelize returns a camelize version of a string 17 | // bob dylan = bobDylan 18 | // widget_id = widgetID 19 | // WidgetID = widgetID 20 | func (i Ident) Camelize() Ident { 21 | var out []string 22 | for i, part := range i.Parts { 23 | var x string 24 | var capped bool 25 | for _, c := range part { 26 | if unicode.IsLetter(c) || unicode.IsDigit(c) { 27 | if i == 0 { 28 | x += string(unicode.ToLower(c)) 29 | continue 30 | } 31 | if !capped { 32 | capped = true 33 | x += string(unicode.ToUpper(c)) 34 | continue 35 | } 36 | x += string(c) 37 | } 38 | } 39 | if x != "" { 40 | out = append(out, x) 41 | } 42 | } 43 | return New(strings.Join(out, "")) 44 | } 45 | -------------------------------------------------------------------------------- /camelize_test.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Camelize(t *testing.T) { 10 | table := []tt{ 11 | {"", ""}, 12 | {"bob dylan", "bobDylan"}, 13 | {"id", "id"}, 14 | {"ID", "id"}, 15 | {"widgetID", "widgetID"}, 16 | {"widget_ID", "widgetID"}, 17 | {"Widget_ID", "widgetID"}, 18 | {"Widget_Id", "widgetID"}, 19 | {"Widget_id", "widgetID"}, 20 | {"Nice to see you!", "niceToSeeYou"}, 21 | {"*hello*", "hello"}, 22 | {"i've read a book! have you?", "iveReadABookHaveYou"}, 23 | {"This is `code` ok", "thisIsCodeOK"}, 24 | {"foo_bar", "fooBar"}, 25 | {"admin/widget", "adminWidget"}, 26 | {"widget", "widget"}, 27 | {"widgets", "widgets"}, 28 | {"status", "status"}, 29 | {"Statuses", "statuses"}, 30 | {"statuses", "statuses"}, 31 | {"People", "people"}, 32 | {"people", "people"}, 33 | {"ip_address", "ipAddress"}, 34 | {"some_url", "someURL"}, 35 | } 36 | 37 | for _, tt := range table { 38 | t.Run(tt.act, func(st *testing.T) { 39 | r := require.New(st) 40 | r.Equal(tt.exp, Camelize(tt.act)) 41 | r.Equal(tt.exp, Camelize(tt.exp)) 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /capitalize.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import "unicode" 4 | 5 | // Capitalize will cap the first letter of string 6 | // user = User 7 | // bob dylan = Bob dylan 8 | // widget_id = Widget_id 9 | func Capitalize(s string) string { 10 | return New(s).Capitalize().String() 11 | } 12 | 13 | // Capitalize will cap the first letter of string 14 | // user = User 15 | // bob dylan = Bob dylan 16 | // widget_id = Widget_id 17 | func (i Ident) Capitalize() Ident { 18 | if len(i.Parts) == 0 { 19 | return New("") 20 | } 21 | runes := []rune(i.Original) 22 | runes[0] = unicode.ToTitle(runes[0]) 23 | return New(string(runes)) 24 | } 25 | -------------------------------------------------------------------------------- /capitalize_test.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Capitalize(t *testing.T) { 10 | table := []tt{ 11 | {"", ""}, 12 | {"foo", "Foo"}, 13 | {"bob dylan", "Bob dylan"}, 14 | {"WidgetID", "WidgetID"}, 15 | {"widget_id", "Widget_id"}, 16 | {"widget_ID", "Widget_ID"}, 17 | {"widget ID", "Widget ID"}, 18 | {"гофер", "Гофер"}, // it's "gopher" in Ukrainian 19 | } 20 | 21 | for _, tt := range table { 22 | t.Run(tt.act, func(st *testing.T) { 23 | r := require.New(st) 24 | r.Equal(tt.exp, Capitalize(tt.act)) 25 | r.Equal(tt.exp, Capitalize(tt.exp)) 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /custom_data.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | func init() { 15 | loadCustomData("inflections.json", "INFLECT_PATH", "could not read inflection file", LoadInflections) 16 | loadCustomData("acronyms.json", "ACRONYMS_PATH", "could not read acronyms file", LoadAcronyms) 17 | } 18 | 19 | //CustomDataParser are functions that parse data like acronyms or 20 | //plurals in the shape of a io.Reader it receives. 21 | type CustomDataParser func(io.Reader) error 22 | 23 | func loadCustomData(defaultFile, env, readErrorMessage string, parser CustomDataParser) { 24 | pwd, _ := os.Getwd() 25 | path, found := os.LookupEnv(env) 26 | if !found { 27 | path = filepath.Join(pwd, defaultFile) 28 | } 29 | 30 | if _, err := os.Stat(path); err != nil { 31 | return 32 | } 33 | 34 | b, err := ioutil.ReadFile(path) 35 | if err != nil { 36 | fmt.Printf("%s %s (%s)\n", readErrorMessage, path, err) 37 | return 38 | } 39 | 40 | if err = parser(bytes.NewReader(b)); err != nil { 41 | fmt.Println(err) 42 | } 43 | } 44 | 45 | //LoadAcronyms loads rules from io.Reader param 46 | func LoadAcronyms(r io.Reader) error { 47 | m := []string{} 48 | err := json.NewDecoder(r).Decode(&m) 49 | 50 | if err != nil { 51 | return fmt.Errorf("could not decode acronyms JSON from reader: %s", err) 52 | } 53 | 54 | acronymsMoot.Lock() 55 | defer acronymsMoot.Unlock() 56 | 57 | for _, acronym := range m { 58 | baseAcronyms[acronym] = true 59 | } 60 | 61 | return nil 62 | } 63 | 64 | //LoadInflections loads rules from io.Reader param 65 | func LoadInflections(r io.Reader) error { 66 | m := map[string]string{} 67 | 68 | err := json.NewDecoder(r).Decode(&m) 69 | if err != nil { 70 | return fmt.Errorf("could not decode inflection JSON from reader: %s", err) 71 | } 72 | 73 | pluralMoot.Lock() 74 | defer pluralMoot.Unlock() 75 | singularMoot.Lock() 76 | defer singularMoot.Unlock() 77 | 78 | for s, p := range m { 79 | if strings.Contains(s, " ") || strings.Contains(p, " ") { 80 | // flect works with parts, so multi-words should not be allowed 81 | return fmt.Errorf("inflection elements should be a single word") 82 | } 83 | singleToPlural[s] = p 84 | pluralToSingle[p] = s 85 | } 86 | 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /dasherize.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | ) 7 | 8 | // Dasherize returns an alphanumeric, lowercased, dashed string 9 | // Donald E. Knuth = donald-e-knuth 10 | // Test with + sign = test-with-sign 11 | // admin/WidgetID = admin-widget-id 12 | func Dasherize(s string) string { 13 | return New(s).Dasherize().String() 14 | } 15 | 16 | // Dasherize returns an alphanumeric, lowercased, dashed string 17 | // Donald E. Knuth = donald-e-knuth 18 | // Test with + sign = test-with-sign 19 | // admin/WidgetID = admin-widget-id 20 | func (i Ident) Dasherize() Ident { 21 | var parts []string 22 | 23 | for _, part := range i.Parts { 24 | var x string 25 | for _, c := range part { 26 | if unicode.IsLetter(c) || unicode.IsDigit(c) { 27 | x += string(c) 28 | } 29 | } 30 | parts = xappend(parts, x) 31 | } 32 | 33 | return New(strings.ToLower(strings.Join(parts, "-"))) 34 | } 35 | -------------------------------------------------------------------------------- /dasherize_test.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Dasherize(t *testing.T) { 10 | table := []tt{ 11 | {"", ""}, 12 | {"admin/WidgetID", "admin-widget-id"}, 13 | {"Donald E. Knuth", "donald-e-knuth"}, 14 | {"Random text with *(bad)* characters", "random-text-with-bad-characters"}, 15 | {"Trailing bad characters!@#", "trailing-bad-characters"}, 16 | {"!@#Leading bad characters", "leading-bad-characters"}, 17 | {"Squeeze separators", "squeeze-separators"}, 18 | {"Test with + sign", "test-with-sign"}, 19 | {"Test with malformed utf8 \251", "test-with-malformed-utf8"}, 20 | } 21 | 22 | for _, tt := range table { 23 | t.Run(tt.act, func(st *testing.T) { 24 | r := require.New(st) 25 | r.Equal(tt.exp, Dasherize(tt.act)) 26 | r.Equal(tt.exp, Dasherize(tt.exp)) 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /flect.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package flect is a new inflection engine to replace [https://github.com/markbates/inflect](https://github.com/markbates/inflect) designed to be more modular, more readable, and easier to fix issues on than the original. 3 | */ 4 | package flect 5 | 6 | import ( 7 | "strings" 8 | "unicode" 9 | ) 10 | 11 | var spaces = []rune{'_', ' ', ':', '-', '/'} 12 | 13 | func isSpace(c rune) bool { 14 | for _, r := range spaces { 15 | if r == c { 16 | return true 17 | } 18 | } 19 | return unicode.IsSpace(c) 20 | } 21 | 22 | func xappend(a []string, ss ...string) []string { 23 | for _, s := range ss { 24 | s = strings.TrimSpace(s) 25 | for _, x := range spaces { 26 | s = strings.Trim(s, string(x)) 27 | } 28 | if _, ok := baseAcronyms[strings.ToUpper(s)]; ok { 29 | s = strings.ToUpper(s) 30 | } 31 | if s != "" { 32 | a = append(a, s) 33 | } 34 | } 35 | return a 36 | } 37 | 38 | func abs(x int) int { 39 | if x < 0 { 40 | return -x 41 | } 42 | return x 43 | } 44 | -------------------------------------------------------------------------------- /flect_test.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | type tt struct { 12 | act string 13 | exp string 14 | } 15 | 16 | func Test_LoadInflections(t *testing.T) { 17 | r := require.New(t) 18 | m := map[string]string{ 19 | "baby": "bebe", 20 | "xyz": "zyx", 21 | } 22 | 23 | b, err := json.Marshal(m) 24 | r.NoError(err) 25 | 26 | r.NoError(LoadInflections(bytes.NewReader(b))) 27 | 28 | for k, v := range m { 29 | r.Equal(v, Pluralize(k)) 30 | r.Equal(v, Pluralize(v)) 31 | r.Equal(k, Singularize(k)) 32 | r.Equal(k, Singularize(v)) 33 | } 34 | } 35 | 36 | func Test_LoadInflectionsWrongSingular(t *testing.T) { 37 | r := require.New(t) 38 | m := map[string]string{ 39 | "a file": "files", 40 | } 41 | 42 | b, err := json.Marshal(m) 43 | r.NoError(err) 44 | 45 | r.Error(LoadInflections(bytes.NewReader(b))) 46 | } 47 | 48 | func Test_LoadInflectionsWrongPlural(t *testing.T) { 49 | r := require.New(t) 50 | m := map[string]string{ 51 | "beatle": "the beatles", 52 | } 53 | 54 | b, err := json.Marshal(m) 55 | r.NoError(err) 56 | 57 | r.Error(LoadInflections(bytes.NewReader(b))) 58 | } 59 | 60 | func Test_LoadAcronyms(t *testing.T) { 61 | r := require.New(t) 62 | m := []string{ 63 | "ACC", 64 | "TLC", 65 | "LSA", 66 | } 67 | 68 | b, err := json.Marshal(m) 69 | r.NoError(err) 70 | 71 | r.NoError(LoadAcronyms(bytes.NewReader(b))) 72 | 73 | for _, acronym := range m { 74 | r.True(baseAcronyms[acronym]) 75 | } 76 | } 77 | 78 | type dict struct { 79 | singular string 80 | plural string 81 | doSingularizeTest bool 82 | doPluralizeTest bool 83 | } 84 | 85 | var singlePluralAssertions = []dict{ 86 | {"", "", true, true}, 87 | {"Car", "Cars", true, true}, 88 | {"Boy", "Boys", true, true}, 89 | {"GoodBoy", "GoodBoys", true, true}, 90 | {"Axis", "Axes", true, true}, 91 | {"Child", "Children", true, true}, 92 | {"GoodChild", "GoodChildren", true, true}, 93 | {"node_child", "node_children", true, true}, 94 | {"SmartPerson", "SmartPeople", true, true}, 95 | {"great_person", "great_people", true, true}, 96 | {"salesperson", "salespeople", true, true}, 97 | {"custom_field", "custom_fields", true, true}, 98 | {"funky jeans", "funky jeans", true, true}, 99 | {"payment_information", "payment_information", true, true}, 100 | {"sportsEquipment", "sportsEquipment", true, true}, 101 | {"status_code", "status_codes", true, true}, 102 | {"user_custom_field", "user_custom_fields", true, true}, 103 | {"SuperbOx", "SuperbOxen", true, true}, 104 | {"WildOx", "WildOxen", true, true}, 105 | {"wild_ox", "wild_oxen", true, true}, 106 | {"box", "boxes", true, true}, 107 | {"fox", "foxes", true, true}, 108 | {"comment", "comments", true, true}, 109 | {"edge", "edges", true, true}, 110 | {"equipment", "equipment", true, true}, 111 | {"experience", "experiences", true, true}, 112 | {"fleet", "fleets", true, true}, 113 | {"foobar", "foobars", true, true}, 114 | {"mouse", "mice", true, true}, 115 | {"newsletter", "newsletters", true, true}, 116 | {"stack", "stacks", true, true}, 117 | {"user", "users", true, true}, 118 | {"woman", "women", true, true}, 119 | {"human", "humans", true, true}, 120 | {"spokesman", "spokesmen", true, true}, 121 | 122 | // Words that end in -f or -fe change -f or -fe to -ves 123 | // https://en.wiktionary.org/wiki/Category:English_irregular_plurals_ending_in_"-ves" 124 | {"calf", "calves", true, true}, 125 | {"dwarf", "dwarves", true, false}, // dwarfs looks popular than dwarves 126 | {"dwarf", "dwarfs", true, true}, 127 | {"elf", "elves", true, true}, 128 | {"half", "halves", true, true}, 129 | {"knife", "knives", true, true}, 130 | {"leaf", "leaves", true, true}, 131 | {"life", "lives", true, true}, 132 | {"loaf", "loaves", true, true}, 133 | {"safe", "saves", true, true}, 134 | {"scarf", "scarves", true, true}, 135 | {"self", "selves", true, true}, 136 | {"sheaf", "sheaves", true, true}, 137 | {"shelf", "shelves", true, true}, 138 | {"thief", "thieves", true, true}, 139 | {"wharf", "wharves", true, true}, 140 | {"wife", "wives", true, true}, 141 | {"wolf", "wolves", true, true}, 142 | {"beef", "beef", true, true}, 143 | {"belief", "beliefs", true, true}, 144 | {"chef", "chefs", true, true}, 145 | {"chief", "chiefs", true, true}, 146 | {"hoof", "hoofs", true, true}, 147 | {"kerchief", "kerchiefs", true, true}, 148 | {"roof", "roofs", true, true}, 149 | {"archive", "archives", true, true}, 150 | {"perspective", "perspectives", true, true}, 151 | 152 | // Words that end in -y preceded by a consonant change -y to -ies 153 | {"day", "days", true, true}, 154 | {"hobby", "hobbies", true, true}, 155 | {"agency", "agencies", true, true}, 156 | {"body", "bodies", true, true}, 157 | {"abbey", "abbeys", true, true}, 158 | {"jiffy", "jiffies", true, true}, 159 | {"geology", "geologies", true, true}, 160 | {"geography", "geographies", true, true}, 161 | {"cookie", "cookies", true, true}, 162 | {"sky", "skies", true, true}, 163 | {"supply", "supplies", true, true}, 164 | {"academy", "academies", true, true}, 165 | {"ebony", "ebonies", true, true}, 166 | {"boy", "boys", true, true}, 167 | {"copy", "copies", true, true}, 168 | {"category", "categories", true, true}, 169 | {"embassy", "embassies", true, true}, 170 | {"entity", "entities", true, true}, 171 | {"guy", "guys", true, true}, 172 | {"obsequy", "obsequies", true, true}, 173 | {"navy", "navies", true, true}, 174 | {"proxy", "proxies", true, true}, 175 | {"crazy", "crazies", true, true}, 176 | 177 | // Words from French that end in -u add an x 178 | {"aboideau", "aboideaux", true, true}, 179 | {"beau", "beaux", true, true}, 180 | {"château", "châteaux", true, true}, 181 | {"chateau", "chateaux", true, true}, 182 | {"fabliau", "fabliaux", true, true}, 183 | {"tableau", "tableaux", true, true}, 184 | {"bureau", "bureaus", true, true}, 185 | {"adieu", "adieux", true, true}, 186 | 187 | // Words from Greek that end in -on change -on to -a 188 | {"tetrahedron", "tetrahedra", true, true}, 189 | 190 | // Words from Latin that end in -um change -um to -a 191 | {"stadium", "stadiums", true, true}, 192 | {"stadium", "stadia", true, false}, 193 | {"aquarium", "aquaria", true, true}, 194 | {"auditorium", "auditoria", true, true}, 195 | {"bacterium", "bacteria", true, true}, 196 | {"pretorium", "pretoriums", true, true}, 197 | {"symposium", "symposia", true, true}, 198 | {"symposium", "symposiums", true, false}, 199 | {"amoebaeum", "amoebaea", true, true}, 200 | {"coliseum", "coliseums", true, true}, 201 | {"museum", "museums", true, true}, 202 | {"agenda", "agendas", true, true}, 203 | {"curriculum", "curriculums", true, true}, 204 | {"collum", "colla", true, true}, 205 | {"datum", "data", true, true}, 206 | {"erratum", "errata", true, true}, 207 | {"maximum", "maxima", true, true}, 208 | {"platinum", "platinums", true, true}, 209 | {"serum", "sera", true, true}, 210 | {"spectrum", "spectra", true, true}, 211 | 212 | // Words from Latin that end in -us change -us to -i or -era 213 | {"opera", "operas", true, true}, 214 | 215 | // Words from Latin that end in -a change -a to -ae 216 | {"alumna", "alumnae", true, true}, 217 | {"larva", "larvae", true, true}, 218 | {"minutia", "minutiae", true, true}, 219 | {"nebula", "nebulae", true, true}, 220 | {"vertebra", "vertebrae", true, true}, 221 | {"vita", "vitae", true, true}, 222 | {"antenna", "antennas", true, true}, 223 | {"formula", "formulas", true, true}, 224 | {"tuna", "tuna", true, true}, 225 | {"quota", "quotas", true, true}, 226 | {"vedalia", "vedalias", true, true}, 227 | {"media", "media", true, true}, // instead of mediae, popular case 228 | {"multimedia", "multimedia", true, true}, 229 | 230 | // Words that end in -ch, -o, -s, -sh, -x, -z 231 | {"lunch", "lunches", true, true}, 232 | {"search", "searches", true, true}, 233 | {"switch", "switches", true, true}, 234 | {"headache", "headaches", true, true}, // ch vs. che 235 | {"marsh", "marshes", true, true}, 236 | {"wish", "wishes", true, true}, 237 | 238 | {"bus", "buses", true, true}, // end with u + s, but no -us rule 239 | {"campus", "campuses", true, true}, 240 | {"caucus", "caucuses", true, true}, 241 | {"circus", "circuses", true, true}, 242 | {"plus", "pluses", true, true}, 243 | {"prometheus", "prometheuses", true, true}, 244 | {"status", "statuses", true, true}, 245 | {"virus", "viruses", true, true}, 246 | {"use", "uses", true, true}, // -use 247 | {"fuse", "fuses", true, true}, 248 | {"house", "houses", true, true}, 249 | {"spouse", "spouses", true, true}, 250 | 251 | {"quiz", "quizzes", true, true}, 252 | {"buzz", "buzzes", true, true}, 253 | {"blitz", "blitzes", true, true}, 254 | {"quartz", "quartzes", true, true}, 255 | {"topaz", "topazes", true, true}, 256 | {"waltz", "waltzes", true, true}, 257 | {"prize", "prizes", true, true}, 258 | 259 | {"access", "accesses", true, true}, 260 | {"process", "processes", true, true}, 261 | {"address", "addresses", true, true}, 262 | {"case", "cases", true, true}, 263 | {"database", "databases", true, true}, 264 | {"glimpse", "glimpses", true, true}, 265 | {"horse", "horses", true, true}, 266 | {"lapse", "lapses", true, true}, 267 | {"collapse", "collapses", true, true}, 268 | {"truss", "trusses", true, true}, 269 | 270 | {"portfolio", "portfolios", true, true}, // -o -os 271 | {"piano", "pianos", true, true}, // -ano -anos 272 | {"hello", "hellos", true, true}, // -lo -los 273 | {"buffalo", "buffaloes", true, true}, // -lo -loes 274 | {"photo", "photos", true, true}, // -to -tos 275 | {"potato", "potatoes", true, true}, // exception of -to -tos 276 | {"tomato", "tomatoes", true, true}, 277 | {"graffiti", "graffiti", true, true}, 278 | {"foo", "foos", true, true}, 279 | {"zoo", "zoos", true, true}, 280 | 281 | // Words from Latin that end in -ex change -ex to -ices 282 | // Words from Latin that end in -ix change -ix to -ices 283 | {"appendix", "appendices", true, true}, // -dix 284 | {"codex", "codices", true, true}, // -dex 285 | {"index", "indices", true, true}, 286 | {"bodice", "bodices", true, true}, // -dice 287 | {"helix", "helices", true, true}, // -lix 288 | {"complex", "complexes", true, true}, // -lex 289 | {"duplex", "duplexes", true, true}, 290 | {"accomplice", "accomplices", true, true}, // -lice 291 | {"slice", "slices", true, true}, 292 | {"matrix", "matrices", true, true}, // -trix 293 | {"justice", "justices", true, true}, // -tice 294 | {"lattice", "lattices", true, true}, 295 | {"notice", "notices", true, true}, 296 | {"apex", "apices", true, true}, // -pex 297 | {"spice", "spices", true, true}, // -pice 298 | {"device", "devices", true, true}, // -vice 299 | {"service", "services", true, true}, 300 | {"fix", "fixes", true, true}, // -ix 301 | {"sex", "sexes", true, true}, // -ex 302 | {"invoice", "invoices", true, true}, // gobuffalo/flect#61 303 | {"voice", "voices", true, true}, 304 | {"choice", "choices", true, true}, 305 | 306 | // Words from Latin that end in -is change -is to -es 307 | {"axis", "axes", true, true}, 308 | {"tax", "taxes", true, true}, // not taxis 309 | {"eclipse", "eclipses", true, true}, 310 | {"ellipse", "ellipses", true, true}, 311 | {"ellipsis", "ellipses", false, true}, // pluralize only 312 | {"oasis", "oases", true, true}, 313 | {"thesis", "theses", true, true}, // word thesis 314 | {"hypothesis", "hypotheses", true, true}, 315 | {"parenthesis", "parentheses", true, true}, 316 | {"analysis", "analyses", true, true}, // suffix lysis 317 | {"antithesis", "antitheses", true, true}, 318 | {"diagnosis", "diagnoses", true, true}, // suffix gnosis 319 | {"prognosis", "prognoses", true, true}, 320 | {"synopsis", "synopses", true, true}, // suffix opsis 321 | {"synapse", "synapses", true, true}, 322 | {"waste", "wastes", true, true}, 323 | {"psi", "psis", true, true}, 324 | {"pepsi", "pepsis", true, true}, 325 | 326 | // Acronyms 327 | {"widget_uuid", "widget_uuids", true, true}, 328 | {"WidgetUUID", "WidgetUUIDs", true, true}, 329 | {"widgetUUID", "widgetUUIDs", true, true}, 330 | {"widgetUuid", "widgetUuids", true, true}, 331 | {"widget_UUID", "widget_UUIDs", true, true}, 332 | 333 | {"ID", "IDs", true, true}, 334 | {"IDS", "IDSes", true, true}, 335 | // id to ids (ID), ids to idses (IDS) is not supported 336 | {"api", "apis", true, true}, 337 | {"API", "APIs", true, true}, 338 | {"html", "htmls", true, true}, 339 | {"HTML", "HTMLs", true, true}, 340 | {"FYI", "FYIs", true, true}, 341 | {"LAN", "LANs", true, true}, 342 | {"ssh", "sshs", true, true}, // sh 343 | {"SSH", "SSHs", true, true}, 344 | {"eia", "eias", true, true}, // ia 345 | {"EIA", "EIAs", true, true}, 346 | {"DNS", "DNSes", true, true}, 347 | } 348 | 349 | func init() { 350 | for _, wd := range dictionary { 351 | if wd.uncountable && wd.plural == "" { 352 | wd.plural = wd.singular 353 | } 354 | 355 | singlePluralAssertions = append(singlePluralAssertions, dict{ 356 | singular: wd.singular, 357 | plural: wd.plural, 358 | doSingularizeTest: !wd.unidirectional, 359 | }) 360 | 361 | if wd.alternative != "" { 362 | singlePluralAssertions = append(singlePluralAssertions, dict{ 363 | singular: wd.singular, 364 | plural: wd.alternative, 365 | doPluralizeTest: false, 366 | }) 367 | } 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gobuffalo/flect 2 | 3 | go 1.16 4 | 5 | exclude github.com/stretchr/testify v1.7.1 6 | 7 | require github.com/stretchr/testify v1.8.1 8 | 9 | retract [v1.0.0, v1.0.1] 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 6 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 7 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 8 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 9 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 13 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 14 | -------------------------------------------------------------------------------- /humanize.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Humanize returns first letter of sentence capitalized. 8 | // Common acronyms are capitalized as well. 9 | // Other capital letters in string are left as provided. 10 | // 11 | // employee_salary = Employee salary 12 | // employee_id = employee ID 13 | // employee_mobile_number = Employee mobile number 14 | // first_Name = First Name 15 | // firstName = First Name 16 | func Humanize(s string) string { 17 | return New(s).Humanize().String() 18 | } 19 | 20 | // Humanize First letter of sentence capitalized 21 | func (i Ident) Humanize() Ident { 22 | if len(i.Original) == 0 { 23 | return New("") 24 | } 25 | 26 | if strings.TrimSpace(i.Original) == "" { 27 | return i 28 | } 29 | 30 | parts := xappend([]string{}, Titleize(i.Parts[0])) 31 | if len(i.Parts) > 1 { 32 | parts = xappend(parts, i.Parts[1:]...) 33 | } 34 | 35 | return New(strings.Join(parts, " ")) 36 | } 37 | -------------------------------------------------------------------------------- /humanize_test.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Humanize(t *testing.T) { 10 | table := []tt{ 11 | {"", ""}, 12 | {"id", "ID"}, 13 | {"url", "URL"}, 14 | {"IBM", "IBM"}, 15 | {"CAUTION! CAPs are CAPs!", "CAUTION! CAPs are CAPs!"}, 16 | {"employee_mobile_number", "Employee mobile number"}, 17 | {"employee_salary", "Employee salary"}, 18 | {"employee_id", "Employee ID"}, 19 | {"employee_ID", "Employee ID"}, 20 | {"first_name", "First name"}, 21 | {"first_Name", "First Name"}, 22 | {"firstName", "First Name"}, 23 | {"óbito", "Óbito"}, 24 | {" ", " "}, 25 | {"\n", "\n"}, 26 | {"\r", "\r"}, 27 | {"\t", "\t"}, 28 | {" \n\r\t", " \n\r\t"}, 29 | } 30 | 31 | for _, tt := range table { 32 | t.Run(tt.act, func(st *testing.T) { 33 | r := require.New(st) 34 | r.Equal(tt.exp, Humanize(tt.act)) 35 | r.Equal(tt.exp, Humanize(tt.exp)) 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ident.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "encoding" 5 | "strings" 6 | "unicode" 7 | "unicode/utf8" 8 | ) 9 | 10 | // Ident represents the string and it's parts 11 | type Ident struct { 12 | Original string 13 | Parts []string 14 | } 15 | 16 | // String implements fmt.Stringer and returns the original string 17 | func (i Ident) String() string { 18 | return i.Original 19 | } 20 | 21 | // New creates a new Ident from the string 22 | func New(s string) Ident { 23 | i := Ident{ 24 | Original: s, 25 | Parts: toParts(s), 26 | } 27 | 28 | return i 29 | } 30 | 31 | func toParts(s string) []string { 32 | parts := []string{} 33 | s = strings.TrimSpace(s) 34 | if len(s) == 0 { 35 | return parts 36 | } 37 | if _, ok := baseAcronyms[strings.ToUpper(s)]; ok { 38 | return []string{strings.ToUpper(s)} 39 | } 40 | var prev rune 41 | var x strings.Builder 42 | x.Grow(len(s)) 43 | for _, c := range s { 44 | // fmt.Println("### cs ->", cs) 45 | // fmt.Println("### unicode.IsControl(c) ->", unicode.IsControl(c)) 46 | // fmt.Println("### unicode.IsDigit(c) ->", unicode.IsDigit(c)) 47 | // fmt.Println("### unicode.IsGraphic(c) ->", unicode.IsGraphic(c)) 48 | // fmt.Println("### unicode.IsLetter(c) ->", unicode.IsLetter(c)) 49 | // fmt.Println("### unicode.IsLower(c) ->", unicode.IsLower(c)) 50 | // fmt.Println("### unicode.IsMark(c) ->", unicode.IsMark(c)) 51 | // fmt.Println("### unicode.IsPrint(c) ->", unicode.IsPrint(c)) 52 | // fmt.Println("### unicode.IsPunct(c) ->", unicode.IsPunct(c)) 53 | // fmt.Println("### unicode.IsSpace(c) ->", unicode.IsSpace(c)) 54 | // fmt.Println("### unicode.IsTitle(c) ->", unicode.IsTitle(c)) 55 | // fmt.Println("### unicode.IsUpper(c) ->", unicode.IsUpper(c)) 56 | if !utf8.ValidRune(c) { 57 | continue 58 | } 59 | 60 | if isSpace(c) { 61 | parts = xappend(parts, x.String()) 62 | x.Reset() 63 | x.WriteRune(c) 64 | prev = c 65 | continue 66 | } 67 | 68 | if unicode.IsUpper(c) && !unicode.IsUpper(prev) { 69 | parts = xappend(parts, x.String()) 70 | x.Reset() 71 | x.WriteRune(c) 72 | prev = c 73 | continue 74 | } 75 | if unicode.IsUpper(c) && baseAcronyms[strings.ToUpper(x.String())] { 76 | parts = xappend(parts, x.String()) 77 | x.Reset() 78 | x.WriteRune(c) 79 | prev = c 80 | continue 81 | } 82 | if unicode.IsLetter(c) || unicode.IsDigit(c) || unicode.IsPunct(c) || c == '`' { 83 | prev = c 84 | x.WriteRune(c) 85 | continue 86 | } 87 | 88 | parts = xappend(parts, x.String()) 89 | x.Reset() 90 | prev = c 91 | } 92 | parts = xappend(parts, x.String()) 93 | 94 | return parts 95 | } 96 | 97 | var _ encoding.TextUnmarshaler = &Ident{} 98 | var _ encoding.TextMarshaler = &Ident{} 99 | 100 | // LastPart returns the last part/word of the original string 101 | func (i *Ident) LastPart() string { 102 | if len(i.Parts) == 0 { 103 | return "" 104 | } 105 | return i.Parts[len(i.Parts)-1] 106 | } 107 | 108 | // ReplaceSuffix creates a new Ident with the original suffix replaced by new 109 | func (i Ident) ReplaceSuffix(orig, new string) Ident { 110 | return New(strings.TrimSuffix(i.Original, orig) + new) 111 | } 112 | 113 | //UnmarshalText unmarshalls byte array into the Ident 114 | func (i *Ident) UnmarshalText(data []byte) error { 115 | (*i) = New(string(data)) 116 | return nil 117 | } 118 | 119 | //MarshalText marshals Ident into byte array 120 | func (i Ident) MarshalText() ([]byte, error) { 121 | return []byte(i.Original), nil 122 | } 123 | -------------------------------------------------------------------------------- /ident_test.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_New(t *testing.T) { 10 | table := []Ident{ 11 | {"", []string{}}, 12 | {"widget", []string{"widget"}}, 13 | {"widget_id", []string{"widget", "ID"}}, 14 | {"WidgetID", []string{"Widget", "ID"}}, 15 | {"Widget_ID", []string{"Widget", "ID"}}, 16 | {"widget_ID", []string{"widget", "ID"}}, 17 | {"widget/ID", []string{"widget", "ID"}}, 18 | {"widgetID", []string{"widget", "ID"}}, 19 | {"widgetName", []string{"widget", "Name"}}, 20 | {"JWTName", []string{"JWT", "Name"}}, 21 | {"JWTname", []string{"JWTname"}}, 22 | {"jwtname", []string{"jwtname"}}, 23 | {"sql", []string{"SQL"}}, 24 | {"sQl", []string{"SQL"}}, 25 | {"id", []string{"ID"}}, 26 | {"Id", []string{"ID"}}, 27 | {"iD", []string{"ID"}}, 28 | {"html", []string{"HTML"}}, 29 | {"Html", []string{"HTML"}}, 30 | {"HTML", []string{"HTML"}}, 31 | {"with `code` inside", []string{"with", "`code`", "inside"}}, 32 | {"Donald E. Knuth", []string{"Donald", "E.", "Knuth"}}, 33 | {"Random text with *(bad)* characters", []string{"Random", "text", "with", "*(bad)*", "characters"}}, 34 | {"Allow_Under_Scores", []string{"Allow", "Under", "Scores"}}, 35 | {"Trailing bad characters!@#", []string{"Trailing", "bad", "characters!@#"}}, 36 | {"!@#Leading bad characters", []string{"!@#", "Leading", "bad", "characters"}}, 37 | {"Squeeze separators", []string{"Squeeze", "separators"}}, 38 | {"Test with + sign", []string{"Test", "with", "sign"}}, 39 | {"Malmö", []string{"Malmö"}}, 40 | {"Garçons", []string{"Garçons"}}, 41 | {"Opsů", []string{"Opsů"}}, 42 | {"Ærøskøbing", []string{"Ærøskøbing"}}, 43 | {"Aßlar", []string{"Aßlar"}}, 44 | {"Japanese: 日本語", []string{"Japanese", "日本語"}}, 45 | } 46 | 47 | for _, tt := range table { 48 | t.Run(tt.Original, func(st *testing.T) { 49 | r := require.New(st) 50 | i := New(tt.Original) 51 | r.Equal(tt.Original, i.Original) 52 | r.Equal(tt.Parts, i.Parts) 53 | }) 54 | } 55 | } 56 | func Test_MarshalText(t *testing.T) { 57 | r := require.New(t) 58 | 59 | n := New("mark") 60 | b, err := n.MarshalText() 61 | r.NoError(err) 62 | r.Equal("mark", string(b)) 63 | 64 | r.NoError((&n).UnmarshalText([]byte("bates"))) 65 | r.Equal("bates", n.String()) 66 | } 67 | 68 | func Benchmark_New(b *testing.B) { 69 | 70 | table := []string{ 71 | "", 72 | "widget", 73 | "widget_id", 74 | "WidgetID", 75 | "Widget_ID", 76 | "widget_ID", 77 | "widget/ID", 78 | "widgetID", 79 | "widgetName", 80 | "JWTName", 81 | "JWTname", 82 | "jwtname", 83 | "sql", 84 | "sQl", 85 | "id", 86 | "Id", 87 | "iD", 88 | "html", 89 | "Html", 90 | "HTML", 91 | "with `code` inside", 92 | "Donald E. Knuth", 93 | "Random text with *(bad)* characters", 94 | "Allow_Under_Scores", 95 | "Trailing bad characters!@#", 96 | "!@#Leading bad characters", 97 | "Squeeze separators", 98 | "Test with + sign", 99 | "Malmö", 100 | "Garçons", 101 | "Opsů", 102 | "Ærøskøbing", 103 | "Aßlar", 104 | "Japanese: 日本語", 105 | } 106 | 107 | for n := 0; n < b.N; n++ { 108 | for i := range table { 109 | New(table[i]) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lower_upper.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import "strings" 4 | 5 | // ToUpper is a convience wrapper for strings.ToUpper 6 | func (i Ident) ToUpper() Ident { 7 | return New(strings.ToUpper(i.Original)) 8 | } 9 | 10 | // ToLower is a convience wrapper for strings.ToLower 11 | func (i Ident) ToLower() Ident { 12 | return New(strings.ToLower(i.Original)) 13 | } 14 | -------------------------------------------------------------------------------- /lower_upper_test.go: -------------------------------------------------------------------------------- 1 | package flect 2 | -------------------------------------------------------------------------------- /name/char.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import "unicode" 4 | 5 | // Char returns the first letter, lowered 6 | // "" = "x" 7 | // "foo" = "f" 8 | // "123d456" = "d" 9 | func Char(s string) string { 10 | return New(s).Char().String() 11 | } 12 | 13 | // Char returns the first letter, lowered 14 | // "" = "x" 15 | // "foo" = "f" 16 | // "123d456" = "d" 17 | func (i Ident) Char() Ident { 18 | for _, c := range i.Original { 19 | if unicode.IsLetter(c) { 20 | return New(string(unicode.ToLower(c))) 21 | } 22 | } 23 | return New("x") 24 | } 25 | -------------------------------------------------------------------------------- /name/char_test.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Char(t *testing.T) { 10 | table := []tt{ 11 | {"", "x"}, 12 | {"foo_bar", "f"}, 13 | {"admin/widget", "a"}, 14 | {"123d4545", "d"}, 15 | {"!@#$%^&*", "x"}, 16 | } 17 | 18 | for _, tt := range table { 19 | t.Run(tt.act, func(st *testing.T) { 20 | r := require.New(st) 21 | r.Equal(tt.exp, Char(tt.act)) 22 | r.Equal(tt.exp, Char(tt.exp)) 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /name/file.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gobuffalo/flect" 7 | ) 8 | 9 | // File creates a suitable file name 10 | // admin/widget = admin/widget 11 | // foo_bar = foo_bar 12 | // U$ser = u_ser 13 | func File(s string, exts ...string) string { 14 | return New(s).File(exts...).String() 15 | } 16 | 17 | // File creates a suitable file name 18 | // admin/widget = admin/widget 19 | // foo_bar = foo_bar 20 | // U$ser = u_ser 21 | func (i Ident) File(exts ...string) Ident { 22 | var parts []string 23 | 24 | for _, part := range strings.Split(i.Original, "/") { 25 | parts = append(parts, flect.Underscore(part)) 26 | } 27 | return New(strings.Join(parts, "/") + strings.Join(exts, "")) 28 | } 29 | -------------------------------------------------------------------------------- /name/file_test.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_File(t *testing.T) { 10 | table := []tt{ 11 | {"", ""}, 12 | {"foo_bar", "foo_bar"}, 13 | {"admin/widget", "admin/widget"}, 14 | {"admin/widgets", "admin/widgets"}, 15 | {"widget", "widget"}, 16 | {"widgets", "widgets"}, 17 | {"User", "user"}, 18 | {"U$er", "u_er"}, 19 | } 20 | 21 | for _, tt := range table { 22 | t.Run(tt.act, func(st *testing.T) { 23 | r := require.New(st) 24 | r.Equal(tt.exp, File(tt.act)) 25 | r.Equal(tt.exp, File(tt.exp)) 26 | r.Equal(tt.exp+".a.b", File(tt.act, ".a", ".b")) 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /name/folder.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/gobuffalo/flect" 8 | ) 9 | 10 | var alphanum = regexp.MustCompile(`[^a-zA-Z0-9_]+`) 11 | 12 | // Folder returns a suitable folder name. It removes any special characters 13 | // from the given string `s` and returns a string consists of alpha-numeric 14 | // characters. 15 | // admin/widget --> admin/widget 16 | // adminUser --> admin_user 17 | // foo_bar --> foo_bar 18 | // Admin --> admin 19 | // U$ser --> u_ser 20 | func Folder(s string, exts ...string) string { 21 | return New(s).Folder(exts...).String() 22 | } 23 | 24 | // Folder returns a suitable folder name. It removes any special characters 25 | // from the given string `s` and returns a string consists of alpha-numeric 26 | // characters. 27 | // admin/widget --> admin/widget 28 | // adminUser --> admin_user 29 | // foo_bar --> foo_bar 30 | // Admin --> admin 31 | // U$ser --> u_ser 32 | func (i Ident) Folder(exts ...string) Ident { 33 | var parts []string 34 | 35 | for _, part := range strings.Split(i.Original, "/") { 36 | part = alphanum.ReplaceAllString(flect.Underscore(part), "") 37 | parts = append(parts, part) 38 | } 39 | 40 | return New(strings.Join(parts, "/") + strings.Join(exts, "")) 41 | } 42 | -------------------------------------------------------------------------------- /name/folder_test.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Folder(t *testing.T) { 10 | table := []tt{ 11 | {"", ""}, 12 | {"admin/widget", "admin/widget"}, 13 | {"admin/widgets", "admin/widgets"}, 14 | {"widget", "widget"}, 15 | {"widgets", "widgets"}, 16 | {"User", "user"}, 17 | {"U$er", "u_er"}, 18 | {"adminuser", "adminuser"}, 19 | {"Adminuser", "adminuser"}, 20 | {"AdminUser", "admin_user"}, 21 | {"adminUser", "admin_user"}, 22 | {"admin-user", "admin_user"}, 23 | {"admin_user", "admin_user"}, 24 | } 25 | 26 | for _, tt := range table { 27 | t.Run(tt.act, func(st *testing.T) { 28 | r := require.New(st) 29 | r.Equal(tt.exp, Folder(tt.act)) 30 | r.Equal(tt.exp, Folder(tt.exp)) 31 | r.Equal(tt.exp+".a.b", Folder(tt.act, ".a", ".b")) 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /name/ident.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import "github.com/gobuffalo/flect" 4 | 5 | // Ident represents the string and it's parts 6 | type Ident struct { 7 | flect.Ident 8 | } 9 | 10 | // New creates a new Ident from the string 11 | func New(s string) Ident { 12 | return Ident{flect.New(s)} 13 | } 14 | -------------------------------------------------------------------------------- /name/interface.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | func Interface(x interface{}) (Ident, error) { 9 | switch t := x.(type) { 10 | case string: 11 | return New(t), nil 12 | default: 13 | rv := reflect.Indirect(reflect.ValueOf(x)) 14 | to := rv.Type() 15 | if len(to.Name()) > 0 { 16 | return New(to.Name()), nil 17 | } 18 | k := to.Kind() 19 | switch k { 20 | case reflect.Slice, reflect.Array: 21 | e := to.Elem() 22 | n := New(e.Name()) 23 | return New(n.Pluralize().String()), nil 24 | } 25 | } 26 | return New(""), fmt.Errorf("could not convert %T to Ident", x) 27 | } 28 | -------------------------------------------------------------------------------- /name/interface_test.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | type car struct{} 11 | 12 | func Test_Interface(t *testing.T) { 13 | table := []struct { 14 | in interface{} 15 | out string 16 | err bool 17 | }{ 18 | {"foo", "foo", false}, 19 | {car{}, "car", false}, 20 | {&car{}, "car", false}, 21 | {[]car{}, "cars", false}, 22 | {false, "bool", false}, 23 | } 24 | 25 | for _, tt := range table { 26 | t.Run(fmt.Sprint(tt.in), func(st *testing.T) { 27 | r := require.New(st) 28 | n, err := Interface(tt.in) 29 | if tt.err { 30 | r.Error(err) 31 | return 32 | } 33 | r.NoError(err) 34 | r.Equal(tt.out, n.String()) 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /name/join.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import "path/filepath" 4 | 5 | func FilePathJoin(names ...string) string { 6 | var ni = make([]Ident, len(names)) 7 | for i, n := range names { 8 | ni[i] = New(n) 9 | } 10 | base := New("") 11 | return base.FilePathJoin(ni...).String() 12 | } 13 | 14 | func (i Ident) FilePathJoin(ni ...Ident) Ident { 15 | var s = make([]string, len(ni)) 16 | for i, n := range ni { 17 | s[i] = n.OsPath().String() 18 | } 19 | return New(filepath.Join(s...)) 20 | } 21 | -------------------------------------------------------------------------------- /name/join_test.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_Ident_FilePathJoin(t *testing.T) { 11 | table := map[string]string{ 12 | "foo/bar/baz": "foo/bar/baz/boo", 13 | "foo\\bar\\baz": "foo/bar/baz/boo", 14 | } 15 | 16 | if runtime.GOOS == "windows" { 17 | table = ident_FilePathJoin_Windows_Table() 18 | } 19 | 20 | for in, out := range table { 21 | t.Run(in, func(st *testing.T) { 22 | r := require.New(st) 23 | r.Equal(out, FilePathJoin(in, "boo")) 24 | }) 25 | } 26 | } 27 | 28 | func ident_FilePathJoin_Windows_Table() map[string]string { 29 | return map[string]string{ 30 | "foo/bar/baz": "foo\\bar\\baz\\boo", 31 | "foo\\bar\\baz": "foo\\bar\\baz\\boo", 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /name/key.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func Key(s string) string { 8 | return New(s).Key().String() 9 | } 10 | 11 | func (i Ident) Key() Ident { 12 | s := strings.Replace(i.String(), "\\", "/", -1) 13 | return New(strings.ToLower(s)) 14 | } 15 | -------------------------------------------------------------------------------- /name/key_test.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Ident_Key(t *testing.T) { 10 | table := map[string]string{ 11 | "Foo/bar/baz": "foo/bar/baz", 12 | "Foo\\bar\\baz": "foo/bar/baz", 13 | } 14 | 15 | for in, out := range table { 16 | t.Run(in, func(st *testing.T) { 17 | r := require.New(st) 18 | r.Equal(out, Key(in)) 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /name/name.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "encoding" 5 | "strings" 6 | 7 | "github.com/gobuffalo/flect" 8 | ) 9 | 10 | // Proper pascalizes and singularizes the string 11 | // person = Person 12 | // foo_bar = FooBar 13 | // admin/widgets = AdminWidget 14 | func Proper(s string) string { 15 | return New(s).Proper().String() 16 | } 17 | 18 | // Proper pascalizes and singularizes the string 19 | // person = Person 20 | // foo_bar = FooBar 21 | // admin/widgets = AdminWidget 22 | func (i Ident) Proper() Ident { 23 | return Ident{i.Singularize().Pascalize()} 24 | } 25 | 26 | // Group pascalizes and pluralizes the string 27 | // person = People 28 | // foo_bar = FooBars 29 | // admin/widget = AdminWidgets 30 | func Group(s string) string { 31 | return New(s).Group().String() 32 | } 33 | 34 | // Group pascalizes and pluralizes the string 35 | // person = People 36 | // foo_bar = FooBars 37 | // admin/widget = AdminWidgets 38 | func (i Ident) Group() Ident { 39 | var parts []string 40 | if len(i.Original) == 0 { 41 | return i 42 | } 43 | last := i.Parts[len(i.Parts)-1] 44 | for _, part := range i.Parts[:len(i.Parts)-1] { 45 | parts = append(parts, flect.Pascalize(part)) 46 | } 47 | last = New(last).Pluralize().Pascalize().String() 48 | parts = append(parts, last) 49 | return New(strings.Join(parts, "")) 50 | } 51 | 52 | var _ encoding.TextUnmarshaler = &Ident{} 53 | var _ encoding.TextMarshaler = &Ident{} 54 | 55 | func (i *Ident) UnmarshalText(data []byte) error { 56 | (*i) = New(string(data)) 57 | return nil 58 | } 59 | 60 | func (i Ident) MarshalText() ([]byte, error) { 61 | return []byte(i.Original), nil 62 | } 63 | -------------------------------------------------------------------------------- /name/name_test.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | type tt struct { 10 | act string 11 | exp string 12 | } 13 | 14 | func Test_Name(t *testing.T) { 15 | table := []tt{ 16 | {"", ""}, 17 | {"bob dylan", "BobDylan"}, 18 | {"widgetID", "WidgetID"}, 19 | {"widget_ID", "WidgetID"}, 20 | {"Widget_ID", "WidgetID"}, 21 | {"Widget_Id", "WidgetID"}, 22 | {"Widget_id", "WidgetID"}, 23 | {"Nice to see you today!", "NiceToSeeYouToday"}, 24 | {"*hello*", "Hello"}, 25 | {"i've read a book! have you read it?", "IveReadABookHaveYouReadIt"}, 26 | {"This is `code` ok", "ThisIsCodeOK"}, 27 | {"foo_bar", "FooBar"}, 28 | {"admin/widget", "AdminWidget"}, 29 | {"admin/widgets", "AdminWidget"}, 30 | {"widget", "Widget"}, 31 | {"widgets", "Widget"}, 32 | {"status", "Status"}, 33 | {"Statuses", "Status"}, 34 | {"statuses", "Status"}, 35 | {"People", "Person"}, 36 | {"people", "Person"}, 37 | } 38 | 39 | for _, tt := range table { 40 | t.Run(tt.act, func(st *testing.T) { 41 | r := require.New(st) 42 | r.Equal(tt.exp, Proper(tt.act)) 43 | r.Equal(tt.exp, Proper(tt.exp)) 44 | }) 45 | } 46 | } 47 | 48 | func Test_Group(t *testing.T) { 49 | table := []tt{ 50 | {"", ""}, 51 | {"Person", "People"}, 52 | {"foo_bar", "FooBars"}, 53 | {"admin/widget", "AdminWidgets"}, 54 | {"widget", "Widgets"}, 55 | {"widgets", "Widgets"}, 56 | {"greatPerson", "GreatPeople"}, 57 | {"great/person", "GreatPeople"}, 58 | {"status", "Statuses"}, 59 | {"Status", "Statuses"}, 60 | {"Statuses", "Statuses"}, 61 | {"statuses", "Statuses"}, 62 | } 63 | 64 | for _, tt := range table { 65 | t.Run(tt.act, func(st *testing.T) { 66 | r := require.New(st) 67 | r.Equal(tt.exp, Group(tt.act)) 68 | r.Equal(tt.exp, Group(tt.exp)) 69 | }) 70 | } 71 | } 72 | 73 | func Test_MarshalText(t *testing.T) { 74 | r := require.New(t) 75 | 76 | n := New("mark") 77 | b, err := n.MarshalText() 78 | r.NoError(err) 79 | r.Equal("mark", string(b)) 80 | 81 | r.NoError((&n).UnmarshalText([]byte("bates"))) 82 | r.Equal("bates", n.String()) 83 | } 84 | -------------------------------------------------------------------------------- /name/os_path.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | "strings" 7 | ) 8 | 9 | func OsPath(s string) string { 10 | return New(s).OsPath().String() 11 | } 12 | 13 | func (i Ident) OsPath() Ident { 14 | s := i.String() 15 | if runtime.GOOS == "windows" { 16 | s = strings.Replace(s, "/", string(filepath.Separator), -1) 17 | } else { 18 | s = strings.Replace(s, "\\", string(filepath.Separator), -1) 19 | } 20 | return New(s) 21 | } 22 | -------------------------------------------------------------------------------- /name/os_path_test.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_Ident_OsPath(t *testing.T) { 11 | table := map[string]string{ 12 | "foo/bar/baz": "foo/bar/baz", 13 | "foo\\bar\\baz": "foo/bar/baz", 14 | } 15 | 16 | if runtime.GOOS == "windows" { 17 | table = ident_OsPath_Windows_Table() 18 | } 19 | 20 | for in, out := range table { 21 | t.Run(in, func(st *testing.T) { 22 | r := require.New(st) 23 | r.Equal(out, OsPath(in)) 24 | }) 25 | } 26 | } 27 | 28 | func ident_OsPath_Windows_Table() map[string]string { 29 | return map[string]string{ 30 | "foo/bar/baz": "foo\\bar\\baz", 31 | "foo\\bar\\baz": "foo\\bar\\baz", 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /name/package.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "go/build" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | // Package will attempt to return a package version of the name 10 | // $GOPATH/src/foo/bar = foo/bar 11 | // $GOPATH\src\foo\bar = foo/bar 12 | // foo/bar = foo/bar 13 | func Package(s string) string { 14 | return New(s).Package().String() 15 | } 16 | 17 | // Package will attempt to return a package version of the name 18 | // $GOPATH/src/foo/bar = foo/bar 19 | // $GOPATH\src\foo\bar = foo/bar 20 | // foo/bar = foo/bar 21 | func (i Ident) Package() Ident { 22 | c := build.Default 23 | 24 | s := i.Original 25 | 26 | for _, src := range c.SrcDirs() { 27 | s = strings.TrimPrefix(s, src) 28 | s = strings.TrimPrefix(s, filepath.Dir(src)) // encase there's no /src prefix 29 | } 30 | 31 | s = strings.TrimPrefix(s, string(filepath.Separator)) 32 | s = strings.Replace(s, "\\", "/", -1) 33 | s = strings.Replace(s, "_", "", -1) 34 | return Ident{New(s).ToLower()} 35 | } 36 | -------------------------------------------------------------------------------- /name/package_test.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "go/build" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_Package(t *testing.T) { 12 | table := []tt{ 13 | {"Foo", "foo"}, 14 | {"Foo/Foo", "foo/foo"}, 15 | {"Foo_Foo", "foofoo"}, 16 | {"create_table", "createtable"}, 17 | {"admin/widget", "admin/widget"}, 18 | {"admin\\widget", "admin/widget"}, 19 | } 20 | 21 | c := build.Default 22 | 23 | for _, src := range c.SrcDirs() { 24 | adds := []tt{ 25 | {filepath.Join(src, "admin/widget"), "admin/widget"}, 26 | {filepath.Join(src, "admin\\widget"), "admin/widget"}, 27 | {filepath.Join(filepath.Dir(src), "admin/widget"), "admin/widget"}, 28 | {filepath.Join(filepath.Dir(src), "admin\\widget"), "admin/widget"}, 29 | } 30 | table = append(table, adds...) 31 | } 32 | for _, tt := range table { 33 | t.Run(tt.act, func(st *testing.T) { 34 | r := require.New(st) 35 | r.Equal(tt.exp, Package(tt.act)) 36 | r.Equal(tt.exp, Package(tt.exp)) 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /name/param_id.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import "strings" 4 | 5 | // ParamID returns the string as parameter with _id added 6 | // user = user_id 7 | // UserID = user_id 8 | // admin/widgets = admin_widgets_id 9 | func ParamID(s string) string { 10 | return New(s).ParamID().String() 11 | } 12 | 13 | // ParamID returns the string as parameter with _id added 14 | // user = user_id 15 | // UserID = user_id 16 | // admin/widgets = admin_widget_id 17 | func (i Ident) ParamID() Ident { 18 | s := i.Singularize().Underscore().String() 19 | s = strings.ToLower(s) 20 | if strings.HasSuffix(s, "_id") { 21 | return New(s) 22 | } 23 | return New(s + "_id") 24 | } 25 | -------------------------------------------------------------------------------- /name/param_id_test.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_ParamID(t *testing.T) { 10 | table := []tt{ 11 | {"foo_bar", "foo_bar_id"}, 12 | {"admin/widget", "admin_widget_id"}, 13 | {"admin/widgets", "admin_widget_id"}, 14 | {"widget", "widget_id"}, 15 | {"User", "user_id"}, 16 | {"user", "user_id"}, 17 | {"UserID", "user_id"}, 18 | } 19 | 20 | for _, tt := range table { 21 | t.Run(tt.act, func(st *testing.T) { 22 | r := require.New(st) 23 | r.Equal(tt.exp, ParamID(tt.act)) 24 | r.Equal(tt.exp, ParamID(tt.exp)) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /name/resource.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Resource version of a name 8 | func (n Ident) Resource() Ident { 9 | name := n.Underscore().String() 10 | x := strings.FieldsFunc(name, func(r rune) bool { 11 | return r == '_' || r == '/' 12 | }) 13 | 14 | for i, w := range x { 15 | if i == len(x)-1 { 16 | x[i] = New(w).Pluralize().Pascalize().String() 17 | continue 18 | } 19 | 20 | x[i] = New(w).Pascalize().String() 21 | } 22 | 23 | return New(strings.Join(x, "")) 24 | } 25 | -------------------------------------------------------------------------------- /name/resource_test.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Name_Resource(t *testing.T) { 10 | r := require.New(t) 11 | table := []struct { 12 | V string 13 | E string 14 | }{ 15 | {V: "Person", E: "People"}, 16 | {V: "foo_bar", E: "FooBars"}, 17 | {V: "admin/widget", E: "AdminWidgets"}, 18 | {V: "widget", E: "Widgets"}, 19 | {V: "widgets", E: "Widgets"}, 20 | {V: "greatPerson", E: "GreatPeople"}, 21 | {V: "great/person", E: "GreatPeople"}, 22 | {V: "status", E: "Statuses"}, 23 | {V: "Status", E: "Statuses"}, 24 | {V: "Statuses", E: "Statuses"}, 25 | {V: "statuses", E: "Statuses"}, 26 | } 27 | for _, tt := range table { 28 | r.Equal(tt.E, New(tt.V).Resource().String()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /name/tablize.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | // Tableize returns an underscore, pluralized string 4 | // User = users 5 | // Person = persons 6 | // Admin/Widget = admin_widgets 7 | func Tableize(s string) string { 8 | return New(s).Tableize().String() 9 | } 10 | 11 | // Tableize returns an underscore, pluralized string 12 | // User = users 13 | // Person = persons 14 | // Admin/Widget = admin_widgets 15 | func (i Ident) Tableize() Ident { 16 | return Ident{i.Underscore().Pluralize()} 17 | } 18 | -------------------------------------------------------------------------------- /name/tablize_test.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Tableize(t *testing.T) { 10 | table := []tt{ 11 | {"", ""}, 12 | {"bob dylan", "bob_dylans"}, 13 | {"Nice to see you!", "nice_to_see_you"}, 14 | {"*hello*", "hellos"}, 15 | {"i've read a book! have you?", "ive_read_a_book_have_you"}, 16 | {"This is `code` ok", "this_is_code_oks"}, 17 | {"foo_bar", "foo_bars"}, 18 | {"admin/widget", "admin_widgets"}, 19 | {"widget", "widgets"}, 20 | {"widgets", "widgets"}, 21 | {"status", "statuses"}, 22 | {"Statuses", "statuses"}, 23 | {"statuses", "statuses"}, 24 | {"People", "people"}, 25 | {"people", "people"}, 26 | {"BigPerson", "big_people"}, 27 | {"Wild Ox", "wild_oxen"}, 28 | } 29 | 30 | for _, tt := range table { 31 | t.Run(tt.act, func(st *testing.T) { 32 | r := require.New(st) 33 | r.Equal(tt.exp, Tableize(tt.act)) 34 | r.Equal(tt.exp, Tableize(tt.exp)) 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /name/url.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | func (n Ident) URL() Ident { 4 | return Ident{n.File().Pluralize()} 5 | } 6 | -------------------------------------------------------------------------------- /name/url_test.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_URL(t *testing.T) { 10 | table := []struct { 11 | in string 12 | out string 13 | }{ 14 | {"User", "users"}, 15 | {"widget", "widgets"}, 16 | {"AdminUser", "admin_users"}, 17 | {"Admin/User", "admin/users"}, 18 | {"Admin/Users", "admin/users"}, 19 | {"/Admin/Users", "/admin/users"}, 20 | } 21 | 22 | for _, tt := range table { 23 | t.Run(tt.in, func(st *testing.T) { 24 | r := require.New(st) 25 | n := New(tt.in) 26 | r.Equal(tt.out, n.URL().String(), "URL of %v", tt.in) 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /name/var_case.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | // VarCaseSingle version of a name. 4 | // foo_bar = fooBar 5 | // admin/widget = adminWidget 6 | // User = users 7 | func VarCaseSingle(s string) string { 8 | return New(s).VarCaseSingle().String() 9 | } 10 | 11 | // VarCaseSingle version of a name. 12 | // foo_bar = fooBar 13 | // admin/widget = adminWidget 14 | // User = users 15 | func (i Ident) VarCaseSingle() Ident { 16 | return Ident{i.Group().Singularize().Camelize()} 17 | } 18 | 19 | // VarCasePlural version of a name. 20 | // foo_bar = fooBars 21 | // admin/widget = adminWidgets 22 | // User = users 23 | func VarCasePlural(s string) string { 24 | return New(s).VarCasePlural().String() 25 | } 26 | 27 | // VarCasePlural version of a name. 28 | // foo_bar = fooBars 29 | // admin/widget = adminWidgets 30 | // User = users 31 | func (i Ident) VarCasePlural() Ident { 32 | return Ident{i.Group().Pluralize().Camelize()} 33 | } 34 | 35 | // VarCase version of a name. 36 | // foo_bar = fooBar 37 | // admin/widget = adminWidget 38 | // Users = users 39 | func (i Ident) VarCase() Ident { 40 | return Ident{i.Camelize()} 41 | } 42 | 43 | // VarCase version of a name. 44 | // foo_bar = fooBar 45 | // admin/widget = adminWidget 46 | // Users = users 47 | func VarCase(s string) string { 48 | return New(s).VarCase().String() 49 | } 50 | -------------------------------------------------------------------------------- /name/var_case_test.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_VarCaseSingle(t *testing.T) { 10 | table := []tt{ 11 | {"foo_bar", "fooBar"}, 12 | {"admin/widget", "adminWidget"}, 13 | {"widget", "widget"}, 14 | {"widgets", "widget"}, 15 | {"User", "user"}, 16 | {"FooBar", "fooBar"}, 17 | {"status", "status"}, 18 | {"statuses", "status"}, 19 | {"Status", "status"}, 20 | {"Statuses", "status"}, 21 | } 22 | 23 | for _, tt := range table { 24 | t.Run(tt.act, func(st *testing.T) { 25 | r := require.New(st) 26 | r.Equal(tt.exp, VarCaseSingle(tt.act)) 27 | r.Equal(tt.exp, VarCaseSingle(tt.exp)) 28 | }) 29 | } 30 | } 31 | 32 | func Test_VarCasePlural(t *testing.T) { 33 | table := []tt{ 34 | {"foo_bar", "fooBars"}, 35 | {"admin/widget", "adminWidgets"}, 36 | {"widget", "widgets"}, 37 | {"widgets", "widgets"}, 38 | {"User", "users"}, 39 | {"FooBar", "fooBars"}, 40 | {"status", "statuses"}, 41 | {"statuses", "statuses"}, 42 | {"Status", "statuses"}, 43 | {"Statuses", "statuses"}, 44 | } 45 | 46 | for _, tt := range table { 47 | t.Run(tt.act, func(st *testing.T) { 48 | r := require.New(st) 49 | r.Equal(tt.exp, VarCasePlural(tt.act)) 50 | r.Equal(tt.exp, VarCasePlural(tt.exp)) 51 | }) 52 | } 53 | } 54 | 55 | func Test_VarCase(t *testing.T) { 56 | table := []tt{ 57 | {"foo_bar", "fooBar"}, 58 | {"admin/widget", "adminWidget"}, 59 | {"widget", "widget"}, 60 | {"widgets", "widgets"}, 61 | {"User", "user"}, 62 | {"FooBar", "fooBar"}, 63 | {"FooBars", "fooBars"}, 64 | {"status", "status"}, 65 | {"statuses", "statuses"}, 66 | {"Status", "status"}, 67 | {"Statuses", "statuses"}, 68 | } 69 | 70 | for _, tt := range table { 71 | t.Run(tt.act, func(st *testing.T) { 72 | r := require.New(st) 73 | r.Equal(tt.exp, VarCase(tt.act)) 74 | r.Equal(tt.exp, VarCase(tt.exp)) 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ordinalize.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | // Ordinalize converts a number to an ordinal version 9 | // 42 = 42nd 10 | // 45 = 45th 11 | // 1 = 1st 12 | func Ordinalize(s string) string { 13 | return New(s).Ordinalize().String() 14 | } 15 | 16 | // Ordinalize converts a number to an ordinal version 17 | // 42 = 42nd 18 | // 45 = 45th 19 | // 1 = 1st 20 | func (i Ident) Ordinalize() Ident { 21 | number, err := strconv.Atoi(i.Original) 22 | if err != nil { 23 | return i 24 | } 25 | var s string 26 | switch abs(number) % 100 { 27 | case 11, 12, 13: 28 | s = fmt.Sprintf("%dth", number) 29 | default: 30 | switch abs(number) % 10 { 31 | case 1: 32 | s = fmt.Sprintf("%dst", number) 33 | case 2: 34 | s = fmt.Sprintf("%dnd", number) 35 | case 3: 36 | s = fmt.Sprintf("%drd", number) 37 | } 38 | } 39 | if s != "" { 40 | return New(s) 41 | } 42 | return New(fmt.Sprintf("%dth", number)) 43 | } 44 | -------------------------------------------------------------------------------- /ordinalize_test.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Ordinalize(t *testing.T) { 10 | table := []tt{ 11 | {"-1", "-1st"}, 12 | {"-2", "-2nd"}, 13 | {"-3", "-3rd"}, 14 | {"-4", "-4th"}, 15 | {"-5", "-5th"}, 16 | {"-6", "-6th"}, 17 | {"-7", "-7th"}, 18 | {"-8", "-8th"}, 19 | {"-9", "-9th"}, 20 | {"-10", "-10th"}, 21 | {"-11", "-11th"}, 22 | {"-12", "-12th"}, 23 | {"-13", "-13th"}, 24 | {"-14", "-14th"}, 25 | {"-20", "-20th"}, 26 | {"-21", "-21st"}, 27 | {"-22", "-22nd"}, 28 | {"-23", "-23rd"}, 29 | {"-24", "-24th"}, 30 | {"-100", "-100th"}, 31 | {"-101", "-101st"}, 32 | {"-102", "-102nd"}, 33 | {"-103", "-103rd"}, 34 | {"-104", "-104th"}, 35 | {"-110", "-110th"}, 36 | {"-111", "-111th"}, 37 | {"-112", "-112th"}, 38 | {"-113", "-113th"}, 39 | {"-1000", "-1000th"}, 40 | {"-1001", "-1001st"}, 41 | {"0", "0th"}, 42 | {"1", "1st"}, 43 | {"2", "2nd"}, 44 | {"3", "3rd"}, 45 | {"4", "4th"}, 46 | {"5", "5th"}, 47 | {"6", "6th"}, 48 | {"7", "7th"}, 49 | {"8", "8th"}, 50 | {"9", "9th"}, 51 | {"10", "10th"}, 52 | {"11", "11th"}, 53 | {"12", "12th"}, 54 | {"13", "13th"}, 55 | {"14", "14th"}, 56 | {"20", "20th"}, 57 | {"21", "21st"}, 58 | {"22", "22nd"}, 59 | {"23", "23rd"}, 60 | {"24", "24th"}, 61 | {"100", "100th"}, 62 | {"101", "101st"}, 63 | {"102", "102nd"}, 64 | {"103", "103rd"}, 65 | {"104", "104th"}, 66 | {"110", "110th"}, 67 | {"111", "111th"}, 68 | {"112", "112th"}, 69 | {"113", "113th"}, 70 | {"1000", "1000th"}, 71 | {"1001", "1001st"}, 72 | } 73 | 74 | for _, tt := range table { 75 | t.Run(tt.act, func(st *testing.T) { 76 | r := require.New(st) 77 | r.Equal(tt.exp, Ordinalize(tt.act)) 78 | r.Equal(tt.exp, Ordinalize(tt.exp)) 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pascalize.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Pascalize returns a string with each segment capitalized 8 | // user = User 9 | // bob dylan = BobDylan 10 | // widget_id = WidgetID 11 | func Pascalize(s string) string { 12 | return New(s).Pascalize().String() 13 | } 14 | 15 | // Pascalize returns a string with each segment capitalized 16 | // user = User 17 | // bob dylan = BobDylan 18 | // widget_id = WidgetID 19 | func (i Ident) Pascalize() Ident { 20 | c := i.Camelize() 21 | if len(c.String()) == 0 { 22 | return c 23 | } 24 | if len(i.Parts) == 0 { 25 | return i 26 | } 27 | capLen := 1 28 | if _, ok := baseAcronyms[strings.ToUpper(i.Parts[0])]; ok { 29 | capLen = len(i.Parts[0]) 30 | } 31 | return New(string(strings.ToUpper(c.Original[0:capLen])) + c.Original[capLen:]) 32 | } 33 | -------------------------------------------------------------------------------- /pascalize_test.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Pascalize(t *testing.T) { 10 | table := []tt{ 11 | {"", ""}, 12 | {"bob dylan", "BobDylan"}, 13 | {"ID", "ID"}, 14 | {"id", "ID"}, 15 | {"widgetID", "WidgetID"}, 16 | {"widget_ID", "WidgetID"}, 17 | {"Widget_ID", "WidgetID"}, 18 | {"Nice to see you!", "NiceToSeeYou"}, 19 | {"*hello*", "Hello"}, 20 | {"i've read a book! have you?", "IveReadABookHaveYou"}, 21 | {"This is `code` ok", "ThisIsCodeOK"}, 22 | {"id", "ID"}, 23 | {"ip_address", "IPAddress"}, 24 | {"some_url", "SomeURL"}, 25 | } 26 | 27 | for _, tt := range table { 28 | t.Run(tt.act, func(st *testing.T) { 29 | r := require.New(st) 30 | r.Equal(tt.exp, Pascalize(tt.act)) 31 | r.Equal(tt.exp, Pascalize(tt.exp)) 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /plural_rules.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import "fmt" 4 | 5 | var pluralRules = []rule{} 6 | 7 | // AddPlural adds a rule that will replace the given suffix with the replacement suffix. 8 | // The name is confusing. This function will be deprecated in the next release. 9 | func AddPlural(suffix string, repl string) { 10 | InsertPluralRule(suffix, repl) 11 | } 12 | 13 | // InsertPluralRule inserts a rule that will replace the given suffix with 14 | // the repl(acement) at the begining of the list of the pluralize rules. 15 | func InsertPluralRule(suffix, repl string) { 16 | pluralMoot.Lock() 17 | defer pluralMoot.Unlock() 18 | 19 | pluralRules = append([]rule{{ 20 | suffix: suffix, 21 | fn: simpleRuleFunc(suffix, repl), 22 | }}, pluralRules...) 23 | 24 | pluralRules = append([]rule{{ 25 | suffix: repl, 26 | fn: noop, 27 | }}, pluralRules...) 28 | } 29 | 30 | type word struct { 31 | singular string 32 | plural string 33 | alternative string 34 | unidirectional bool // plural to singular is not possible (or bad) 35 | uncountable bool 36 | exact bool 37 | } 38 | 39 | // dictionary is the main table for singularize and pluralize. 40 | // All words in the dictionary will be added to singleToPlural, pluralToSingle 41 | // and singlePluralAssertions by init() functions. 42 | var dictionary = []word{ 43 | // identicals https://en.wikipedia.org/wiki/English_plurals#Nouns_with_identical_singular_and_plural 44 | {singular: "aircraft", plural: "aircraft"}, 45 | {singular: "beef", plural: "beef", alternative: "beefs"}, 46 | {singular: "bison", plural: "bison"}, 47 | {singular: "blues", plural: "blues", unidirectional: true}, 48 | {singular: "chassis", plural: "chassis"}, 49 | {singular: "deer", plural: "deer"}, 50 | {singular: "fish", plural: "fish", alternative: "fishes"}, 51 | {singular: "moose", plural: "moose"}, 52 | {singular: "police", plural: "police"}, 53 | {singular: "salmon", plural: "salmon", alternative: "salmons"}, 54 | {singular: "series", plural: "series"}, 55 | {singular: "sheep", plural: "sheep"}, 56 | {singular: "shrimp", plural: "shrimp", alternative: "shrimps"}, 57 | {singular: "species", plural: "species"}, 58 | {singular: "swine", plural: "swine", alternative: "swines"}, 59 | {singular: "trout", plural: "trout", alternative: "trouts"}, 60 | {singular: "tuna", plural: "tuna", alternative: "tunas"}, 61 | {singular: "you", plural: "you"}, 62 | // -en https://en.wikipedia.org/wiki/English_plurals#Plurals_in_-(e)n 63 | {singular: "child", plural: "children"}, 64 | {singular: "ox", plural: "oxen", exact: true}, 65 | // apophonic https://en.wikipedia.org/wiki/English_plurals#Apophonic_plurals 66 | {singular: "foot", plural: "feet"}, 67 | {singular: "goose", plural: "geese"}, 68 | {singular: "man", plural: "men"}, 69 | {singular: "human", plural: "humans"}, // not humen 70 | {singular: "louse", plural: "lice", exact: true}, 71 | {singular: "mouse", plural: "mice"}, 72 | {singular: "tooth", plural: "teeth"}, 73 | {singular: "woman", plural: "women"}, 74 | // misc https://en.wikipedia.org/wiki/English_plurals#Miscellaneous_irregular_plurals 75 | {singular: "die", plural: "dice", exact: true}, 76 | {singular: "person", plural: "people"}, 77 | 78 | // Words from French that end in -u add an x; in addition to eau to eaux rule 79 | {singular: "adieu", plural: "adieux", alternative: "adieus"}, 80 | {singular: "fabliau", plural: "fabliaux"}, 81 | {singular: "bureau", plural: "bureaus", alternative: "bureaux"}, // popular 82 | 83 | // Words from Greek that end in -on change -on to -a; in addition to hedron rule 84 | {singular: "criterion", plural: "criteria"}, 85 | {singular: "ganglion", plural: "ganglia", alternative: "ganglions"}, 86 | {singular: "lexicon", plural: "lexica", alternative: "lexicons"}, 87 | {singular: "mitochondrion", plural: "mitochondria", alternative: "mitochondrions"}, 88 | {singular: "noumenon", plural: "noumena"}, 89 | {singular: "phenomenon", plural: "phenomena"}, 90 | {singular: "taxon", plural: "taxa"}, 91 | 92 | // Words from Latin that end in -um change -um to -a; in addition to some rules 93 | {singular: "media", plural: "media"}, // popular case: media -> media 94 | {singular: "medium", plural: "media", alternative: "mediums", unidirectional: true}, 95 | {singular: "stadium", plural: "stadiums", alternative: "stadia"}, 96 | {singular: "aquarium", plural: "aquaria", alternative: "aquariums"}, 97 | {singular: "auditorium", plural: "auditoria", alternative: "auditoriums"}, 98 | {singular: "symposium", plural: "symposia", alternative: "symposiums"}, 99 | {singular: "curriculum", plural: "curriculums", alternative: "curricula"}, // ulum 100 | {singular: "quota", plural: "quotas"}, 101 | 102 | // Words from Latin that end in -us change -us to -i or -era 103 | {singular: "alumnus", plural: "alumni", alternative: "alumnuses"}, // -i 104 | {singular: "bacillus", plural: "bacilli"}, 105 | {singular: "cactus", plural: "cacti", alternative: "cactuses"}, 106 | {singular: "coccus", plural: "cocci"}, 107 | {singular: "focus", plural: "foci", alternative: "focuses"}, 108 | {singular: "locus", plural: "loci", alternative: "locuses"}, 109 | {singular: "nucleus", plural: "nuclei", alternative: "nucleuses"}, 110 | {singular: "octopus", plural: "octupuses", alternative: "octopi"}, 111 | {singular: "radius", plural: "radii", alternative: "radiuses"}, 112 | {singular: "syllabus", plural: "syllabi"}, 113 | {singular: "corpus", plural: "corpora", alternative: "corpuses"}, // -ra 114 | {singular: "genus", plural: "genera"}, 115 | 116 | // Words from Latin that end in -a change -a to -ae 117 | {singular: "alumna", plural: "alumnae"}, 118 | {singular: "vertebra", plural: "vertebrae"}, 119 | {singular: "differentia", plural: "differentiae"}, // -tia 120 | {singular: "minutia", plural: "minutiae"}, 121 | {singular: "vita", plural: "vitae"}, // -ita 122 | {singular: "larva", plural: "larvae"}, // -va 123 | {singular: "postcava", plural: "postcavae"}, 124 | {singular: "praecava", plural: "praecavae"}, 125 | {singular: "uva", plural: "uvae"}, 126 | 127 | // Words from Latin that end in -ex change -ex to -ices 128 | {singular: "apex", plural: "apices", alternative: "apexes"}, 129 | {singular: "codex", plural: "codices", alternative: "codexes"}, 130 | {singular: "index", plural: "indices", alternative: "indexes"}, 131 | {singular: "latex", plural: "latices", alternative: "latexes"}, 132 | {singular: "vertex", plural: "vertices", alternative: "vertexes"}, 133 | {singular: "vortex", plural: "vortices", alternative: "vortexes"}, 134 | 135 | // Words from Latin that end in -ix change -ix to -ices (eg, matrix becomes matrices) 136 | {singular: "appendix", plural: "appendices", alternative: "appendixes"}, 137 | {singular: "radix", plural: "radices", alternative: "radixes"}, 138 | {singular: "helix", plural: "helices", alternative: "helixes"}, 139 | 140 | // Words from Latin that end in -is change -is to -es 141 | {singular: "axis", plural: "axes", exact: true}, 142 | {singular: "crisis", plural: "crises"}, 143 | {singular: "ellipsis", plural: "ellipses", unidirectional: true}, // ellipse 144 | {singular: "genesis", plural: "geneses"}, 145 | {singular: "oasis", plural: "oases"}, 146 | {singular: "thesis", plural: "theses"}, 147 | {singular: "testis", plural: "testes"}, 148 | {singular: "base", plural: "bases"}, // popular case 149 | {singular: "basis", plural: "bases", unidirectional: true}, 150 | 151 | {singular: "alias", plural: "aliases", exact: true}, // no alia, no aliasis 152 | {singular: "vedalia", plural: "vedalias"}, // no vedalium, no vedaliases 153 | 154 | // Words that end in -ch, -o, -s, -sh, -x, -z (can be conflict with the others) 155 | {singular: "use", plural: "uses", exact: true}, // us vs use 156 | {singular: "abuse", plural: "abuses"}, 157 | {singular: "cause", plural: "causes"}, 158 | {singular: "clause", plural: "clauses"}, 159 | {singular: "cruse", plural: "cruses"}, 160 | {singular: "excuse", plural: "excuses"}, 161 | {singular: "fuse", plural: "fuses"}, 162 | {singular: "house", plural: "houses"}, 163 | {singular: "misuse", plural: "misuses"}, 164 | {singular: "muse", plural: "muses"}, 165 | {singular: "pause", plural: "pauses"}, 166 | {singular: "ache", plural: "aches"}, 167 | {singular: "topaz", plural: "topazes"}, 168 | {singular: "buffalo", plural: "buffaloes", alternative: "buffalos"}, 169 | {singular: "potato", plural: "potatoes"}, 170 | {singular: "tomato", plural: "tomatoes"}, 171 | 172 | // uncountables 173 | {singular: "equipment", uncountable: true}, 174 | {singular: "information", uncountable: true}, 175 | {singular: "jeans", uncountable: true}, 176 | {singular: "money", uncountable: true}, 177 | {singular: "news", uncountable: true}, 178 | {singular: "rice", uncountable: true}, 179 | 180 | // exceptions: -f to -ves, not -fe 181 | {singular: "dwarf", plural: "dwarfs", alternative: "dwarves"}, 182 | {singular: "hoof", plural: "hoofs", alternative: "hooves"}, 183 | {singular: "thief", plural: "thieves"}, 184 | // exceptions: instead of -f(e) to -ves 185 | {singular: "chive", plural: "chives"}, 186 | {singular: "hive", plural: "hives"}, 187 | {singular: "move", plural: "moves"}, 188 | 189 | // exceptions: instead of -y to -ies 190 | {singular: "movie", plural: "movies"}, 191 | {singular: "cookie", plural: "cookies"}, 192 | 193 | // exceptions: instead of -um to -a 194 | {singular: "pretorium", plural: "pretoriums"}, 195 | {singular: "agenda", plural: "agendas"}, // instead of plural of agendum 196 | // exceptions: instead of -um to -a (chemical element names) 197 | 198 | // Words from Latin that end in -a change -a to -ae 199 | {singular: "formula", plural: "formulas", alternative: "formulae"}, // also -um/-a 200 | 201 | // exceptions: instead of -o to -oes 202 | {singular: "shoe", plural: "shoes"}, 203 | {singular: "toe", plural: "toes", exact: true}, 204 | {singular: "graffiti", plural: "graffiti"}, 205 | 206 | // abbreviations 207 | {singular: "ID", plural: "IDs", exact: true}, 208 | } 209 | 210 | // singleToPlural is the highest priority map for Pluralize(). 211 | // singularToPluralSuffixList is used to build pluralRules for suffixes and 212 | // compound words. 213 | var singleToPlural = map[string]string{} 214 | 215 | // pluralToSingle is the highest priority map for Singularize(). 216 | // singularToPluralSuffixList is used to build singularRules for suffixes and 217 | // compound words. 218 | var pluralToSingle = map[string]string{} 219 | 220 | // NOTE: This map should not be built as reverse map of singleToPlural since 221 | // there are words that has the same plurals. 222 | 223 | // build singleToPlural and pluralToSingle with dictionary 224 | func init() { 225 | for _, wd := range dictionary { 226 | if singleToPlural[wd.singular] != "" { 227 | panic(fmt.Errorf("map singleToPlural already has an entry for %s", wd.singular)) 228 | } 229 | 230 | if wd.uncountable && wd.plural == "" { 231 | wd.plural = wd.singular 232 | } 233 | 234 | if wd.plural == "" { 235 | panic(fmt.Errorf("plural for %s is not provided", wd.singular)) 236 | } 237 | 238 | singleToPlural[wd.singular] = wd.plural 239 | 240 | if !wd.unidirectional { 241 | if pluralToSingle[wd.plural] != "" { 242 | panic(fmt.Errorf("map pluralToSingle already has an entry for %s", wd.plural)) 243 | } 244 | pluralToSingle[wd.plural] = wd.singular 245 | 246 | if wd.alternative != "" { 247 | if pluralToSingle[wd.alternative] != "" { 248 | panic(fmt.Errorf("map pluralToSingle already has an entry for %s", wd.alternative)) 249 | } 250 | pluralToSingle[wd.alternative] = wd.singular 251 | } 252 | } 253 | } 254 | } 255 | 256 | type singularToPluralSuffix struct { 257 | singular string 258 | plural string 259 | } 260 | 261 | // singularToPluralSuffixList is a list of "bidirectional" suffix rules for 262 | // the irregular plurals follow such rules. 263 | // 264 | // NOTE: IMPORTANT! The order of items in this list is the rule priority, not 265 | // alphabet order. The first match will be used to inflect. 266 | var singularToPluralSuffixList = []singularToPluralSuffix{ 267 | // https://en.wiktionary.org/wiki/Appendix:English_irregular_nouns#Rules 268 | // Words that end in -f or -fe change -f or -fe to -ves 269 | {"tive", "tives"}, // exception 270 | {"eaf", "eaves"}, 271 | {"oaf", "oaves"}, 272 | {"afe", "aves"}, 273 | {"arf", "arves"}, 274 | {"rfe", "rves"}, 275 | {"rf", "rves"}, 276 | {"lf", "lves"}, 277 | {"fe", "ves"}, // previously '[a-eg-km-z]fe' TODO: regex support 278 | 279 | // Words that end in -y preceded by a consonant change -y to -ies 280 | {"ay", "ays"}, 281 | {"ey", "eys"}, 282 | {"oy", "oys"}, 283 | {"quy", "quies"}, 284 | {"uy", "uys"}, 285 | {"y", "ies"}, // '[^aeiou]y' 286 | 287 | // Words from French that end in -u add an x (eg, château becomes châteaux) 288 | {"eau", "eaux"}, // it seems like 'eau' is the most popular form of this rule 289 | 290 | // Words from Latin that end in -a change -a to -ae; before -on to -a and -um to -a 291 | {"bula", "bulae"}, 292 | {"dula", "bulae"}, 293 | {"lula", "bulae"}, 294 | {"nula", "bulae"}, 295 | {"vula", "bulae"}, 296 | 297 | // Words from Greek that end in -on change -on to -a (eg, polyhedron becomes polyhedra) 298 | // https://en.wiktionary.org/wiki/Category:English_irregular_plurals_ending_in_"-a" 299 | {"hedron", "hedra"}, 300 | 301 | // Words from Latin that end in -um change -um to -a (eg, minimum becomes minima) 302 | // https://en.wiktionary.org/wiki/Category:English_irregular_plurals_ending_in_"-a" 303 | {"ium", "ia"}, // some exceptions especially chemical element names 304 | {"seum", "seums"}, 305 | {"eum", "ea"}, 306 | {"oum", "oa"}, 307 | {"stracum", "straca"}, 308 | {"dum", "da"}, 309 | {"elum", "ela"}, 310 | {"ilum", "ila"}, 311 | {"olum", "ola"}, 312 | {"ulum", "ula"}, 313 | {"llum", "lla"}, 314 | {"ylum", "yla"}, 315 | {"imum", "ima"}, 316 | {"ernum", "erna"}, 317 | {"gnum", "gna"}, 318 | {"brum", "bra"}, 319 | {"crum", "cra"}, 320 | {"terum", "tera"}, 321 | {"serum", "sera"}, 322 | {"trum", "tra"}, 323 | {"antum", "anta"}, 324 | {"atum", "ata"}, 325 | {"entum", "enta"}, 326 | {"etum", "eta"}, 327 | {"itum", "ita"}, 328 | {"otum", "ota"}, 329 | {"utum", "uta"}, 330 | {"ctum", "cta"}, 331 | {"ovum", "ova"}, 332 | 333 | // Words from Latin that end in -us change -us to -i or -era 334 | // not easy to make a simple rule. just add them all to the dictionary 335 | 336 | // Words from Latin that end in -ex change -ex to -ices (eg, vortex becomes vortices) 337 | // Words from Latin that end in -ix change -ix to -ices (eg, matrix becomes matrices) 338 | // for example, -dix, -dex, and -dice will have the same plural form so 339 | // making a simple rule is not possible for them 340 | {"trix", "trices"}, // ignore a few words end in trice 341 | 342 | // Words from Latin that end in -is change -is to -es (eg, thesis becomes theses) 343 | // -sis and -se has the same plural -ses so making a rule is not easy too. 344 | {"iasis", "iases"}, 345 | {"mesis", "meses"}, 346 | {"kinesis", "kineses"}, 347 | {"resis", "reses"}, 348 | {"gnosis", "gnoses"}, // e.g. diagnosis 349 | {"opsis", "opses"}, // e.g. synopsis 350 | {"ysis", "yses"}, // e.g. analysis 351 | 352 | // Words that end in -ch, -o, -s, -sh, -x, -z 353 | {"ouse", "ouses"}, 354 | {"lause", "lauses"}, 355 | {"us", "uses"}, // use/uses is in the dictionary 356 | 357 | {"ch", "ches"}, 358 | {"io", "ios"}, 359 | {"sh", "shes"}, 360 | {"ss", "sses"}, 361 | {"ez", "ezzes"}, 362 | {"iz", "izzes"}, 363 | {"tz", "tzes"}, 364 | {"zz", "zzes"}, 365 | {"ano", "anos"}, 366 | {"lo", "los"}, 367 | {"to", "tos"}, 368 | {"oo", "oos"}, 369 | {"o", "oes"}, 370 | {"x", "xes"}, 371 | 372 | // for abbreviations 373 | {"S", "Ses"}, 374 | 375 | // excluded rules: seems rare 376 | // Words from Hebrew that add -im or -ot (eg, cherub becomes cherubim) 377 | // - cherub (cherubs or cherubim), seraph (seraphs or seraphim) 378 | // Words from Greek that end in -ma change -ma to -mata 379 | // - The most of words end in -ma are in this category but it looks like 380 | // just adding -s is more popular. 381 | // Words from Latin that end in -nx change -nx to -nges 382 | // - The most of words end in -nx are in this category but it looks like 383 | // just adding -es is more popular. (sphinxes) 384 | 385 | // excluded rules: don't care at least for now: 386 | // Words that end in -ful that add an s after the -ful 387 | // Words that end in -s or -ese denoting a national of a particular country 388 | // Symbols or letters, which often add -'s 389 | } 390 | 391 | func init() { 392 | for i := len(singularToPluralSuffixList) - 1; i >= 0; i-- { 393 | InsertPluralRule(singularToPluralSuffixList[i].singular, singularToPluralSuffixList[i].plural) 394 | InsertSingularRule(singularToPluralSuffixList[i].plural, singularToPluralSuffixList[i].singular) 395 | } 396 | 397 | // build pluralRule and singularRule with dictionary for compound words 398 | for _, wd := range dictionary { 399 | if wd.exact { 400 | continue 401 | } 402 | 403 | if wd.uncountable && wd.plural == "" { 404 | wd.plural = wd.singular 405 | } 406 | 407 | InsertPluralRule(wd.singular, wd.plural) 408 | 409 | if !wd.unidirectional { 410 | InsertSingularRule(wd.plural, wd.singular) 411 | 412 | if wd.alternative != "" { 413 | InsertSingularRule(wd.alternative, wd.singular) 414 | } 415 | } 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /pluralize.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | ) 7 | 8 | var pluralMoot = &sync.RWMutex{} 9 | 10 | // Pluralize returns a plural version of the string 11 | // user = users 12 | // person = people 13 | // datum = data 14 | func Pluralize(s string) string { 15 | return New(s).Pluralize().String() 16 | } 17 | 18 | // PluralizeWithSize will pluralize a string taking a number number into account. 19 | // PluralizeWithSize("user", 1) = user 20 | // PluralizeWithSize("user", 2) = users 21 | func PluralizeWithSize(s string, i int) string { 22 | if i == 1 || i == -1 { 23 | return New(s).Singularize().String() 24 | } 25 | return New(s).Pluralize().String() 26 | } 27 | 28 | // Pluralize returns a plural version of the string 29 | // user = users 30 | // person = people 31 | // datum = data 32 | func (i Ident) Pluralize() Ident { 33 | s := i.LastPart() 34 | if len(s) == 0 { 35 | return New("") 36 | } 37 | 38 | pluralMoot.RLock() 39 | defer pluralMoot.RUnlock() 40 | 41 | // check if the Original has an explicit entry in the map 42 | if p, ok := singleToPlural[i.Original]; ok { 43 | return i.ReplaceSuffix(i.Original, p) 44 | } 45 | if _, ok := pluralToSingle[i.Original]; ok { 46 | return i 47 | } 48 | 49 | ls := strings.ToLower(s) 50 | if _, ok := pluralToSingle[ls]; ok { 51 | return i 52 | } 53 | 54 | if p, ok := singleToPlural[ls]; ok { 55 | if s == Capitalize(s) { 56 | p = Capitalize(p) 57 | } 58 | return i.ReplaceSuffix(s, p) 59 | } 60 | 61 | for _, r := range pluralRules { 62 | if strings.HasSuffix(s, r.suffix) { 63 | return i.ReplaceSuffix(s, r.fn(s)) 64 | } 65 | } 66 | 67 | if strings.HasSuffix(ls, "s") { 68 | return i 69 | } 70 | 71 | return New(i.String() + "s") 72 | } 73 | -------------------------------------------------------------------------------- /pluralize_test.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Pluralize(t *testing.T) { 10 | for _, tt := range singlePluralAssertions { 11 | if tt.doPluralizeTest { 12 | t.Run(tt.singular, func(st *testing.T) { 13 | r := require.New(st) 14 | r.Equal(tt.plural, Pluralize(tt.singular), "pluralize %s", tt.singular) 15 | r.Equal(tt.plural, Pluralize(tt.plural), "pluralize %s", tt.plural) 16 | }) 17 | } 18 | } 19 | } 20 | 21 | func Test_PluralizeWithSize(t *testing.T) { 22 | for _, tt := range singlePluralAssertions { 23 | t.Run(tt.singular, func(st *testing.T) { 24 | r := require.New(st) 25 | if tt.doSingularizeTest { 26 | r.Equal(tt.singular, PluralizeWithSize(tt.singular, -1), "pluralize %d %s", -1, tt.singular) 27 | r.Equal(tt.singular, PluralizeWithSize(tt.plural, -1), "pluralize %d %s", -1, tt.plural) 28 | r.Equal(tt.singular, PluralizeWithSize(tt.singular, 1), "pluralize %d %s", 1, tt.singular) 29 | r.Equal(tt.singular, PluralizeWithSize(tt.plural, 1), "pluralize %d %s", 1, tt.plural) 30 | } 31 | if tt.doPluralizeTest { 32 | r.Equal(tt.plural, PluralizeWithSize(tt.singular, -2), "pluralize %d %s", -2, tt.singular) 33 | r.Equal(tt.plural, PluralizeWithSize(tt.plural, -2), "pluralize %d %s", -2, tt.plural) 34 | r.Equal(tt.plural, PluralizeWithSize(tt.singular, 0), "pluralize %d %s", 0, tt.singular) 35 | r.Equal(tt.plural, PluralizeWithSize(tt.plural, 0), "pluralize %d %s", 0, tt.plural) 36 | r.Equal(tt.plural, PluralizeWithSize(tt.singular, 2), "pluralize %d %s", 2, tt.singular) 37 | r.Equal(tt.plural, PluralizeWithSize(tt.plural, 2), "pluralize %d %s", 2, tt.plural) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func BenchmarkPluralize(b *testing.B) { 44 | for n := 0; n < b.N; n++ { 45 | for _, tt := range singlePluralAssertions { 46 | if tt.doPluralizeTest { 47 | Pluralize(tt.singular) 48 | Pluralize(tt.plural) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /rule.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | type ruleFn func(string) string 4 | 5 | type rule struct { 6 | suffix string 7 | fn ruleFn 8 | } 9 | 10 | func simpleRuleFunc(suffix, repl string) func(string) string { 11 | return func(s string) string { 12 | s = s[:len(s)-len(suffix)] 13 | return s + repl 14 | } 15 | } 16 | 17 | func noop(s string) string { return s } 18 | -------------------------------------------------------------------------------- /singular_rules.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | var singularRules = []rule{} 4 | 5 | // AddSingular adds a rule that will replace the given suffix with the replacement suffix. 6 | // The name is confusing. This function will be deprecated in the next release. 7 | func AddSingular(ext string, repl string) { 8 | InsertSingularRule(ext, repl) 9 | } 10 | 11 | // InsertSingularRule inserts a rule that will replace the given suffix with 12 | // the repl(acement) at the beginning of the list of the singularize rules. 13 | func InsertSingularRule(suffix, repl string) { 14 | singularMoot.Lock() 15 | defer singularMoot.Unlock() 16 | 17 | singularRules = append([]rule{{ 18 | suffix: suffix, 19 | fn: simpleRuleFunc(suffix, repl), 20 | }}, singularRules...) 21 | 22 | singularRules = append([]rule{{ 23 | suffix: repl, 24 | fn: noop, 25 | }}, singularRules...) 26 | } 27 | -------------------------------------------------------------------------------- /singularize.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | ) 7 | 8 | var singularMoot = &sync.RWMutex{} 9 | 10 | // Singularize returns a singular version of the string 11 | // users = user 12 | // data = datum 13 | // people = person 14 | func Singularize(s string) string { 15 | return New(s).Singularize().String() 16 | } 17 | 18 | // SingularizeWithSize will singular a string taking a number number into account. 19 | // SingularizeWithSize("user", 1) = user 20 | // SingularizeWithSize("user", 2) = users 21 | func SingularizeWithSize(s string, i int) string { 22 | return PluralizeWithSize(s, i) 23 | } 24 | 25 | // Singularize returns a singular version of the string 26 | // users = user 27 | // data = datum 28 | // people = person 29 | func (i Ident) Singularize() Ident { 30 | s := i.LastPart() 31 | if len(s) == 0 { 32 | return i 33 | } 34 | 35 | singularMoot.RLock() 36 | defer singularMoot.RUnlock() 37 | 38 | // check if the Original has an explicit entry in the map 39 | if p, ok := pluralToSingle[i.Original]; ok { 40 | return i.ReplaceSuffix(i.Original, p) 41 | } 42 | if _, ok := singleToPlural[i.Original]; ok { 43 | return i 44 | } 45 | 46 | ls := strings.ToLower(s) 47 | if p, ok := pluralToSingle[ls]; ok { 48 | if s == Capitalize(s) { 49 | p = Capitalize(p) 50 | } 51 | return i.ReplaceSuffix(s, p) 52 | } 53 | 54 | if _, ok := singleToPlural[ls]; ok { 55 | return i 56 | } 57 | 58 | for _, r := range singularRules { 59 | if strings.HasSuffix(s, r.suffix) { 60 | return i.ReplaceSuffix(s, r.fn(s)) 61 | } 62 | } 63 | 64 | if strings.HasSuffix(s, "s") { 65 | return i.ReplaceSuffix("s", "") 66 | } 67 | 68 | return i 69 | } 70 | -------------------------------------------------------------------------------- /singularize_test.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Singularize(t *testing.T) { 10 | for _, tt := range singlePluralAssertions { 11 | if tt.doSingularizeTest { 12 | t.Run(tt.plural, func(st *testing.T) { 13 | r := require.New(st) 14 | r.Equal(tt.singular, Singularize(tt.plural), "singularize %s", tt.plural) 15 | r.Equal(tt.singular, Singularize(tt.singular), "singularize %s", tt.singular) 16 | }) 17 | } 18 | } 19 | } 20 | 21 | func Test_SingularizeWithSize(t *testing.T) { 22 | for _, tt := range singlePluralAssertions { 23 | t.Run(tt.plural, func(st *testing.T) { 24 | r := require.New(st) 25 | if tt.doSingularizeTest { 26 | r.Equal(tt.singular, SingularizeWithSize(tt.plural, -1), "singularize %d %s", -1, tt.plural) 27 | r.Equal(tt.singular, SingularizeWithSize(tt.singular, -1), "singularize %d %s", -1, tt.singular) 28 | r.Equal(tt.singular, SingularizeWithSize(tt.plural, 1), "singularize %d %s", 1, tt.plural) 29 | r.Equal(tt.singular, SingularizeWithSize(tt.singular, 1), "singularize %d %s", 1, tt.singular) 30 | } 31 | if tt.doPluralizeTest { 32 | r.Equal(tt.plural, SingularizeWithSize(tt.plural, -2), "singularize %d %s", -2, tt.plural) 33 | r.Equal(tt.plural, SingularizeWithSize(tt.singular, -2), "singularize %d %s", -2, tt.singular) 34 | r.Equal(tt.plural, SingularizeWithSize(tt.plural, 0), "singularize %d %s", 0, tt.plural) 35 | r.Equal(tt.plural, SingularizeWithSize(tt.singular, 0), "singularize %d %s", 0, tt.singular) 36 | r.Equal(tt.plural, SingularizeWithSize(tt.plural, 2), "singularize %d %s", 2, tt.plural) 37 | r.Equal(tt.plural, SingularizeWithSize(tt.singular, 2), "singularize %d %s", 2, tt.singular) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func BenchmarkSingularize(b *testing.B) { 44 | for n := 0; n < b.N; n++ { 45 | for _, tt := range singlePluralAssertions { 46 | if tt.doSingularizeTest { 47 | Singularize(tt.singular) 48 | Singularize(tt.plural) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /titleize.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | ) 7 | 8 | // Titleize will capitalize the start of each part 9 | // "Nice to see you!" = "Nice To See You!" 10 | // "i've read a book! have you?" = "I've Read A Book! Have You?" 11 | // "This is `code` ok" = "This Is `code` OK" 12 | func Titleize(s string) string { 13 | return New(s).Titleize().String() 14 | } 15 | 16 | // Titleize will capitalize the start of each part 17 | // "Nice to see you!" = "Nice To See You!" 18 | // "i've read a book! have you?" = "I've Read A Book! Have You?" 19 | // "This is `code` ok" = "This Is `code` OK" 20 | func (i Ident) Titleize() Ident { 21 | var parts []string 22 | 23 | // TODO: we need to reconsider the design. 24 | // this approach preserves inline code block as is but it also 25 | // preserves the other words start with a special character. 26 | // I would prefer: "*wonderful* world" to be "*Wonderful* World" 27 | for _, part := range i.Parts { 28 | // CAUTION: in unicode, []rune(str)[0] is not rune(str[0]) 29 | runes := []rune(part) 30 | x := string(unicode.ToTitle(runes[0])) 31 | if len(runes) > 1 { 32 | x += string(runes[1:]) 33 | } 34 | parts = append(parts, x) 35 | } 36 | 37 | return New(strings.Join(parts, " ")) 38 | } 39 | -------------------------------------------------------------------------------- /titleize_test.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Titleize(t *testing.T) { 10 | table := []tt{ 11 | {"", ""}, 12 | {"bob dylan", "Bob Dylan"}, 13 | {"Nice to see you!", "Nice To See You!"}, 14 | {"*hello*", "*hello*"}, 15 | {"hello *wonderful* world!", "Hello *wonderful* World!"}, // CHKME 16 | {"i've read a book! have you?", "I've Read A Book! Have You?"}, 17 | {"This is `code` ok", "This Is `code` OK"}, 18 | {"foo_bar", "Foo Bar"}, 19 | {"admin/widget", "Admin Widget"}, 20 | {"widget", "Widget"}, 21 | {"óbito", "Óbito"}, 22 | } 23 | 24 | for _, tt := range table { 25 | t.Run(tt.act, func(st *testing.T) { 26 | r := require.New(st) 27 | r.Equal(tt.exp, Titleize(tt.act)) 28 | r.Equal(tt.exp, Titleize(tt.exp)) 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /underscore.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | ) 7 | 8 | // Underscore a string 9 | // bob dylan --> bob_dylan 10 | // Nice to see you! --> nice_to_see_you 11 | // widgetID --> widget_id 12 | func Underscore(s string) string { 13 | return New(s).Underscore().String() 14 | } 15 | 16 | // Underscore a string 17 | // bob dylan --> bob_dylan 18 | // Nice to see you! --> nice_to_see_you 19 | // widgetID --> widget_id 20 | func (i Ident) Underscore() Ident { 21 | out := make([]string, 0, len(i.Parts)) 22 | for _, part := range i.Parts { 23 | var x strings.Builder 24 | x.Grow(len(part)) 25 | for _, c := range part { 26 | if unicode.IsLetter(c) || unicode.IsDigit(c) { 27 | x.WriteRune(c) 28 | } 29 | } 30 | if x.Len() > 0 { 31 | out = append(out, x.String()) 32 | } 33 | } 34 | return New(strings.ToLower(strings.Join(out, "_"))) 35 | } 36 | -------------------------------------------------------------------------------- /underscore_test.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Underscore(t *testing.T) { 10 | baseAcronyms["TLC"] = true 11 | 12 | table := []tt{ 13 | {"", ""}, 14 | {"bob dylan", "bob_dylan"}, 15 | {"Nice to see you!", "nice_to_see_you"}, 16 | {"*hello*", "hello"}, 17 | {"i've read a book! have you?", "ive_read_a_book_have_you"}, 18 | {"This is `code` ok", "this_is_code_ok"}, 19 | {"TLCForm", "tlc_form"}, 20 | } 21 | 22 | for _, tt := range table { 23 | t.Run(tt.act, func(st *testing.T) { 24 | r := require.New(st) 25 | r.Equal(tt.exp, Underscore(tt.act)) 26 | r.Equal(tt.exp, Underscore(tt.exp)) 27 | }) 28 | } 29 | } 30 | 31 | func Benchmark_Underscore(b *testing.B) { 32 | 33 | table := []string{ 34 | "", 35 | "bob dylan", 36 | "Nice to see you!", 37 | "*hello*", 38 | "i've read a book! have you?", 39 | "This is `code` ok", 40 | "TLCForm", 41 | } 42 | 43 | for n := 0; n < b.N; n++ { 44 | for i := range table { 45 | Underscore(table[i]) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package flect 2 | 3 | //Version holds Flect version number 4 | const Version = "v1.0.0" 5 | --------------------------------------------------------------------------------