├── example_test.go ├── .travis.yml ├── README.md ├── CakePHP_LICENSE.txt ├── LICENSE.md ├── inflector_test.go └── inflector.go /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Akeda Bagus . All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | package inflector_test 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/gedex/inflector" 11 | ) 12 | 13 | func ExampleSingularize() { 14 | fmt.Println(inflector.Singularize("People")) 15 | // Output: 16 | // Person 17 | } 18 | 19 | func ExamplePluralize() { 20 | fmt.Println(inflector.Pluralize("octopus")) 21 | // Output: 22 | // octopuses 23 | } 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: Go 2 | go: 3 | - 1.5.4 4 | - 1.6.3 5 | - 1.7 6 | - tip 7 | matrix: 8 | include: 9 | - go: 1.4.3 10 | script: 11 | - go get -t -v ./... 12 | - go test -v -race ./... 13 | allow_failures: 14 | - go: tip 15 | fast_finish: true 16 | install: 17 | - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). 18 | script: 19 | - go get -t -v ./... 20 | - diff -u <(echo -n) <(gofmt -d -s .) 21 | - go tool vet . 22 | - go test -v -race ./... 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Inflector 2 | ========= 3 | 4 | Inflector pluralizes and singularizes English nouns. 5 | 6 | [![Build Status](https://travis-ci.org/gedex/inflector.png?branch=master)](https://travis-ci.org/gedex/inflector) 7 | [![Coverage Status](https://coveralls.io/repos/gedex/inflector/badge.png?branch=master)](https://coveralls.io/r/gedex/inflector?branch=master) 8 | [![GoDoc](https://godoc.org/github.com/gedex/inflector?status.svg)](https://godoc.org/github.com/gedex/inflector) 9 | 10 | ## Basic Usage 11 | 12 | There are only two exported functions: `Pluralize` and `Singularize`. 13 | 14 | ~~~go 15 | fmt.Println(inflector.Singularize("People")) // will print "Person" 16 | fmt.Println(inflector.Pluralize("octopus")) // will print "octopuses" 17 | ~~~ 18 | 19 | ## Credits 20 | 21 | * [CakePHP's Inflector](https://github.com/cakephp/cakephp/blob/master/lib/Cake/Utility/Inflector.php) 22 | 23 | ## License 24 | 25 | This library is distributed under the BSD-style license found in the LICENSE.md file. 26 | -------------------------------------------------------------------------------- /CakePHP_LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | CakePHP(tm) : The Rapid Development PHP Framework (http://cakephp.org) 4 | Copyright (c) 2005-2013, Cake Software Foundation, Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the "Software"), 8 | to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22 | DEALINGS IN THE SOFTWARE. 23 | 24 | Cake Software Foundation, Inc. 25 | 1785 E. Sahara Avenue, 26 | Suite 490-204 27 | Las Vegas, Nevada 89104, 28 | United States of America. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Akeda Bagus . All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 15 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 17 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 18 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 19 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 20 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | ---------- 27 | 28 | Much of this library was inspired from CakePHP's inflector, a PHP 29 | framework licensed under MIT license (see CakePHP_LICENSE.txt). 30 | -------------------------------------------------------------------------------- /inflector_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Akeda Bagus . All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package inflector 7 | 8 | import ( 9 | "testing" 10 | ) 11 | 12 | type inflector func(string) string 13 | 14 | type inflectorTest struct { 15 | in string 16 | out string 17 | match bool 18 | } 19 | 20 | var pluralTests = []inflectorTest{ 21 | {"categoria", "categorias", true}, 22 | {"house", "houses", true}, 23 | {"powerhouse", "powerhouses", true}, 24 | {"Bus", "Buses", true}, 25 | {"bus", "buses", true}, 26 | {"menu", "menus", true}, 27 | {"news", "news", true}, 28 | {"food_menu", "food_menus", true}, 29 | {"Menu", "Menus", true}, 30 | {"FoodMenu", "FoodMenus", true}, 31 | {"quiz", "quizzes", true}, 32 | {"matrix_row", "matrix_rows", true}, 33 | {"matrix", "matrices", true}, 34 | {"vertex", "vertices", true}, 35 | {"index", "indices", true}, 36 | {"Alias", "Aliases", true}, 37 | {"Aliases", "Aliases", false}, 38 | {"Media", "Media", true}, 39 | {"NodeMedia", "NodeMedia", true}, 40 | {"alumnus", "alumni", true}, 41 | {"bacillus", "bacilli", true}, 42 | {"cactus", "cacti", true}, 43 | {"focus", "foci", true}, 44 | {"fungus", "fungi", true}, 45 | {"nucleus", "nuclei", true}, 46 | {"octopus", "octopuses", true}, 47 | {"radius", "radii", true}, 48 | {"stimulus", "stimuli", true}, 49 | {"syllabus", "syllabi", true}, 50 | {"terminus", "termini", true}, 51 | {"virus", "viri", true}, 52 | {"person", "people", true}, 53 | {"people", "people", false}, 54 | {"glove", "gloves", true}, 55 | {"crisis", "crises", true}, 56 | {"tax", "taxes", true}, 57 | {"wave", "waves", true}, 58 | {"bureau", "bureaus", true}, 59 | {"cafe", "cafes", true}, 60 | {"roof", "roofs", true}, 61 | {"foe", "foes", true}, 62 | {"cookie", "cookies", true}, 63 | {"wolf", "wolves", true}, 64 | {"thief", "thieves", true}, 65 | {"potato", "potatoes", true}, 66 | {"hero", "heroes", true}, 67 | {"buffalo", "buffalo", true}, 68 | {"tooth", "teeth", true}, 69 | {"goose", "geese", true}, 70 | {"foot", "feet", true}, 71 | {"objective", "objectives", true}, 72 | {"specie", "species", false}, 73 | {"species", "species", true}, 74 | {"", "", true}, 75 | } 76 | 77 | var singularTests = []inflectorTest{ 78 | {"categorias", "categoria", true}, 79 | {"menus", "menu", true}, 80 | {"news", "news", true}, 81 | {"food_menus", "food_menu", true}, 82 | {"Menus", "Menu", true}, 83 | {"FoodMenus", "FoodMenu", true}, 84 | {"houses", "house", true}, 85 | {"powerhouses", "powerhouse", true}, 86 | {"quizzes", "quiz", true}, 87 | {"Buses", "Bus", true}, 88 | {"buses", "bus", true}, 89 | {"matrix_rows", "matrix_row", true}, 90 | {"matrices", "matrix", true}, 91 | {"vertices", "vertex", true}, 92 | {"indices", "index", true}, 93 | {"Aliases", "Alias", true}, 94 | {"Alias", "Alias", false}, 95 | {"Media", "Media", true}, 96 | {"NodeMedia", "NodeMedia", true}, 97 | {"alumni", "alumnus", true}, 98 | {"bacilli", "bacillus", true}, 99 | {"cacti", "cactus", true}, 100 | {"foci", "focus", true}, 101 | {"fungi", "fungus", true}, 102 | {"nuclei", "nucleus", true}, 103 | {"octopuses", "octopus", true}, 104 | {"radii", "radius", true}, 105 | {"stimuli", "stimulus", true}, 106 | {"syllabi", "syllabus", true}, 107 | {"termini", "terminus", true}, 108 | {"viri", "virus", true}, 109 | {"people", "person", true}, 110 | {"gloves", "glove", true}, 111 | {"doves", "dove", true}, 112 | {"lives", "life", true}, 113 | {"knives", "knife", true}, 114 | {"wolves", "wolf", true}, 115 | {"slaves", "slave", true}, 116 | {"shelves", "shelf", true}, 117 | {"taxis", "taxi", true}, 118 | {"taxes", "tax", true}, 119 | {"Taxes", "Tax", true}, 120 | {"AwesomeTaxes", "AwesomeTax", true}, 121 | {"faxes", "fax", true}, 122 | {"waxes", "wax", true}, 123 | {"niches", "niche", true}, 124 | {"waves", "wave", true}, 125 | {"bureaus", "bureau", true}, 126 | {"genetic_analyses", "genetic_analysis", true}, 127 | {"doctor_diagnoses", "doctor_diagnosis", true}, 128 | {"parantheses", "paranthesis", true}, 129 | {"Causes", "Cause", true}, 130 | {"colossuses", "colossus", true}, 131 | {"diagnoses", "diagnosis", true}, 132 | {"bases", "basis", true}, 133 | {"analyses", "analysis", true}, 134 | {"curves", "curve", true}, 135 | {"cafes", "cafe", true}, 136 | {"roofs", "roof", true}, 137 | {"foes", "foe", true}, 138 | {"databases", "database", true}, 139 | {"cookies", "cookie", true}, 140 | {"thieves", "thief", true}, 141 | {"potatoes", "potato", true}, 142 | {"heroes", "hero", true}, 143 | {"buffalos", "buffalo", false}, 144 | {"babies", "baby", true}, 145 | {"teeth", "tooth", true}, 146 | {"geese", "goose", true}, 147 | {"feet", "foot", true}, 148 | {"objectives", "objective", true}, 149 | {"species", "species", true}, 150 | {"", "", true}, 151 | } 152 | 153 | func TestPluralize(t *testing.T) { 154 | for i, test := range pluralTests { 155 | s := Pluralize(test.in) 156 | if s != test.out { 157 | t.Errorf("test %d Pluralize(%s) = %s, expected: %s", i, test.in, s, test.out) 158 | } 159 | 160 | // Second retrieval should returns the same result. 161 | // This is also tests the cache 162 | s = Pluralize(test.in) 163 | if s != test.out { 164 | t.Errorf("test %d (2) Pluralize(%s) = %s, expected: %s", i, test.in, s, test.out) 165 | } 166 | } 167 | } 168 | 169 | func TestSingularize(t *testing.T) { 170 | for i, test := range singularTests { 171 | s := Singularize(test.in) 172 | if s != test.out { 173 | t.Errorf("test %d Singularize(%s) = %s, expected: %s", i, test.in, s, test.out) 174 | } 175 | 176 | // Second retrieval should returns the same result. 177 | // This is also tests the cache 178 | s = Singularize(test.in) 179 | if s != test.out { 180 | t.Errorf("test %d (2) Singularize(%s) = %s, expected: %s", i, test.in, s, test.out) 181 | } 182 | } 183 | } 184 | 185 | func TestReverse(t *testing.T) { 186 | for i, test := range pluralTests { 187 | if !test.match { 188 | continue 189 | } 190 | s := Singularize(Pluralize(test.in)) 191 | if s != test.in { 192 | t.Errorf("test %d Singularize(Pluralize(%s)) != %s, got: %s", i, test.in, test.in, s) 193 | } 194 | } 195 | 196 | for i, test := range singularTests { 197 | if !test.match { 198 | continue 199 | } 200 | s := Pluralize(Singularize(test.in)) 201 | if s != test.in { 202 | t.Errorf("test %d Pluralize(Singularize(%s)) != %s, got: %s", i, test.in, test.in, s) 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /inflector.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Akeda Bagus . All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | /* 7 | Package inflector pluralizes and singularizes English nouns. 8 | 9 | There are only two exported functions: `Pluralize` and `Singularize`. 10 | 11 | s := "People" 12 | fmt.Println(inflector.Singularize(s)) // will print "Person" 13 | 14 | s2 := "octopus" 15 | fmt.Println(inflector.Pluralize(s2)) // will print "octopuses" 16 | 17 | */ 18 | package inflector 19 | 20 | import ( 21 | "bytes" 22 | "fmt" 23 | "regexp" 24 | "strings" 25 | "sync" 26 | ) 27 | 28 | // Rule represents name of the inflector rule, can be 29 | // Plural or Singular 30 | type Rule int 31 | 32 | const ( 33 | Plural = iota 34 | Singular 35 | ) 36 | 37 | // InflectorRule represents inflector rule 38 | type InflectorRule struct { 39 | Rules []*ruleItem 40 | Irregular []*irregularItem 41 | Uninflected []string 42 | compiledIrregular *regexp.Regexp 43 | compiledUninflected *regexp.Regexp 44 | compiledRules []*compiledRule 45 | } 46 | 47 | type ruleItem struct { 48 | pattern string 49 | replacement string 50 | } 51 | 52 | type irregularItem struct { 53 | word string 54 | replacement string 55 | } 56 | 57 | // compiledRule represents compiled version of Inflector.Rules. 58 | type compiledRule struct { 59 | replacement string 60 | *regexp.Regexp 61 | } 62 | 63 | // threadsafe access to rules and caches 64 | var mutex sync.Mutex 65 | var rules = make(map[Rule]*InflectorRule) 66 | 67 | // Words that should not be inflected 68 | var uninflected = []string{ 69 | `Amoyese`, `bison`, `Borghese`, `bream`, `breeches`, `britches`, `buffalo`, 70 | `cantus`, `carp`, `chassis`, `clippers`, `cod`, `coitus`, `Congoese`, 71 | `contretemps`, `corps`, `debris`, `diabetes`, `djinn`, `eland`, `elk`, 72 | `equipment`, `Faroese`, `flounder`, `Foochowese`, `gallows`, `Genevese`, 73 | `Genoese`, `Gilbertese`, `graffiti`, `headquarters`, `herpes`, `hijinks`, 74 | `Hottentotese`, `information`, `innings`, `jackanapes`, `Kiplingese`, 75 | `Kongoese`, `Lucchese`, `mackerel`, `Maltese`, `.*?media`, `mews`, `moose`, 76 | `mumps`, `Nankingese`, `news`, `nexus`, `Niasese`, `Pekingese`, 77 | `Piedmontese`, `pincers`, `Pistoiese`, `pliers`, `Portuguese`, `proceedings`, 78 | `rabies`, `rice`, `rhinoceros`, `salmon`, `Sarawakese`, `scissors`, 79 | `sea[- ]bass`, `series`, `Shavese`, `shears`, `siemens`, `species`, `swine`, 80 | `testes`, `trousers`, `trout`, `tuna`, `Vermontese`, `Wenchowese`, `whiting`, 81 | `wildebeest`, `Yengeese`, 82 | } 83 | 84 | // Plural words that should not be inflected 85 | var uninflectedPlurals = []string{ 86 | `.*[nrlm]ese`, `.*deer`, `.*fish`, `.*measles`, `.*ois`, `.*pox`, `.*sheep`, 87 | `people`, 88 | } 89 | 90 | // Singular words that should not be inflected 91 | var uninflectedSingulars = []string{ 92 | `.*[nrlm]ese`, `.*deer`, `.*fish`, `.*measles`, `.*ois`, `.*pox`, `.*sheep`, 93 | `.*ss`, 94 | } 95 | 96 | type cache map[string]string 97 | 98 | // Inflected words that already cached for immediate retrieval from a given Rule 99 | var caches = make(map[Rule]cache) 100 | 101 | // map of irregular words where its key is a word and its value is the replacement 102 | var irregularMaps = make(map[Rule]cache) 103 | 104 | func init() { 105 | 106 | rules[Plural] = &InflectorRule{ 107 | Rules: []*ruleItem{ 108 | {`(?i)(s)tatus$`, `${1}${2}tatuses`}, 109 | {`(?i)(quiz)$`, `${1}zes`}, 110 | {`(?i)^(ox)$`, `${1}${2}en`}, 111 | {`(?i)([m|l])ouse$`, `${1}ice`}, 112 | {`(?i)(matr|vert|ind)(ix|ex)$`, `${1}ices`}, 113 | {`(?i)(x|ch|ss|sh)$`, `${1}es`}, 114 | {`(?i)([^aeiouy]|qu)y$`, `${1}ies`}, 115 | {`(?i)(hive)$`, `$1s`}, 116 | {`(?i)(?:([^f])fe|([lre])f)$`, `${1}${2}ves`}, 117 | {`(?i)sis$`, `ses`}, 118 | {`(?i)([ti])um$`, `${1}a`}, 119 | {`(?i)(p)erson$`, `${1}eople`}, 120 | {`(?i)(m)an$`, `${1}en`}, 121 | {`(?i)(c)hild$`, `${1}hildren`}, 122 | {`(?i)(buffal|tomat)o$`, `${1}${2}oes`}, 123 | {`(?i)(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$`, `${1}i`}, 124 | {`(?i)us$`, `uses`}, 125 | {`(?i)(alias)$`, `${1}es`}, 126 | {`(?i)(ax|cris|test)is$`, `${1}es`}, 127 | {`s$`, `s`}, 128 | {`^$`, ``}, 129 | {`$`, `s`}, 130 | }, 131 | Irregular: []*irregularItem{ 132 | {`atlas`, `atlases`}, 133 | {`beef`, `beefs`}, 134 | {`brother`, `brothers`}, 135 | {`cafe`, `cafes`}, 136 | {`child`, `children`}, 137 | {`cookie`, `cookies`}, 138 | {`corpus`, `corpuses`}, 139 | {`cow`, `cows`}, 140 | {`ganglion`, `ganglions`}, 141 | {`genie`, `genies`}, 142 | {`genus`, `genera`}, 143 | {`graffito`, `graffiti`}, 144 | {`hoof`, `hoofs`}, 145 | {`loaf`, `loaves`}, 146 | {`man`, `men`}, 147 | {`money`, `monies`}, 148 | {`mongoose`, `mongooses`}, 149 | {`move`, `moves`}, 150 | {`mythos`, `mythoi`}, 151 | {`niche`, `niches`}, 152 | {`numen`, `numina`}, 153 | {`occiput`, `occiputs`}, 154 | {`octopus`, `octopuses`}, 155 | {`opus`, `opuses`}, 156 | {`ox`, `oxen`}, 157 | {`penis`, `penises`}, 158 | {`person`, `people`}, 159 | {`sex`, `sexes`}, 160 | {`soliloquy`, `soliloquies`}, 161 | {`testis`, `testes`}, 162 | {`trilby`, `trilbys`}, 163 | {`turf`, `turfs`}, 164 | {`potato`, `potatoes`}, 165 | {`hero`, `heroes`}, 166 | {`tooth`, `teeth`}, 167 | {`goose`, `geese`}, 168 | {`foot`, `feet`}, 169 | }, 170 | } 171 | prepare(Plural) 172 | 173 | rules[Singular] = &InflectorRule{ 174 | Rules: []*ruleItem{ 175 | {`(?i)(s)tatuses$`, `${1}${2}tatus`}, 176 | {`(?i)^(.*)(menu)s$`, `${1}${2}`}, 177 | {`(?i)(quiz)zes$`, `$1`}, 178 | {`(?i)(matr)ices$`, `${1}ix`}, 179 | {`(?i)(vert|ind)ices$`, `${1}ex`}, 180 | {`(?i)^(ox)en`, `$1`}, 181 | {`(?i)(alias)(es)*$`, `$1`}, 182 | {`(?i)(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$`, `${1}us`}, 183 | {`(?i)([ftw]ax)es`, `$1`}, 184 | {`(?i)(cris|ax|test)es$`, `${1}is`}, 185 | {`(?i)(shoe|slave)s$`, `$1`}, 186 | {`(?i)(o)es$`, `$1`}, 187 | {`ouses$`, `ouse`}, 188 | {`([^a])uses$`, `${1}us`}, 189 | {`(?i)([m|l])ice$`, `${1}ouse`}, 190 | {`(?i)(x|ch|ss|sh)es$`, `$1`}, 191 | {`(?i)(m)ovies$`, `${1}${2}ovie`}, 192 | {`(?i)(s)eries$`, `${1}${2}eries`}, 193 | {`(?i)([^aeiouy]|qu)ies$`, `${1}y`}, 194 | {`(?i)(tive)s$`, `$1`}, 195 | {`(?i)([lre])ves$`, `${1}f`}, 196 | {`(?i)([^fo])ves$`, `${1}fe`}, 197 | {`(?i)(hive)s$`, `$1`}, 198 | {`(?i)(drive)s$`, `$1`}, 199 | {`(?i)(^analy)ses$`, `${1}sis`}, 200 | {`(?i)(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$`, `${1}${2}sis`}, 201 | {`(?i)([ti])a$`, `${1}um`}, 202 | {`(?i)(p)eople$`, `${1}${2}erson`}, 203 | {`(?i)(m)en$`, `${1}an`}, 204 | {`(?i)(c)hildren$`, `${1}${2}hild`}, 205 | {`(?i)(n)ews$`, `${1}${2}ews`}, 206 | {`eaus$`, `eau`}, 207 | {`^(.*us)$`, `$1`}, 208 | {`(?i)s$`, ``}, 209 | }, 210 | Irregular: []*irregularItem{ 211 | {`foes`, `foe`}, 212 | {`waves`, `wave`}, 213 | {`curves`, `curve`}, 214 | {`atlases`, `atlas`}, 215 | {`beefs`, `beef`}, 216 | {`brothers`, `brother`}, 217 | {`cafes`, `cafe`}, 218 | {`children`, `child`}, 219 | {`cookies`, `cookie`}, 220 | {`corpuses`, `corpus`}, 221 | {`cows`, `cow`}, 222 | {`ganglions`, `ganglion`}, 223 | {`genies`, `genie`}, 224 | {`genera`, `genus`}, 225 | {`graffiti`, `graffito`}, 226 | {`hoofs`, `hoof`}, 227 | {`loaves`, `loaf`}, 228 | {`men`, `man`}, 229 | {`monies`, `money`}, 230 | {`mongooses`, `mongoose`}, 231 | {`moves`, `move`}, 232 | {`mythoi`, `mythos`}, 233 | {`niches`, `niche`}, 234 | {`numina`, `numen`}, 235 | {`occiputs`, `occiput`}, 236 | {`octopuses`, `octopus`}, 237 | {`opuses`, `opus`}, 238 | {`oxen`, `ox`}, 239 | {`penises`, `penis`}, 240 | {`people`, `person`}, 241 | {`sexes`, `sex`}, 242 | {`soliloquies`, `soliloquy`}, 243 | {`testes`, `testis`}, 244 | {`trilbys`, `trilby`}, 245 | {`turfs`, `turf`}, 246 | {`potatoes`, `potato`}, 247 | {`heroes`, `hero`}, 248 | {`teeth`, `tooth`}, 249 | {`geese`, `goose`}, 250 | {`feet`, `foot`}, 251 | }, 252 | } 253 | prepare(Singular) 254 | } 255 | 256 | // prepare rule, e.g., compile the pattern. 257 | func prepare(r Rule) error { 258 | var reString string 259 | 260 | switch r { 261 | case Plural: 262 | // Merge global uninflected with singularsUninflected 263 | rules[r].Uninflected = merge(uninflected, uninflectedPlurals) 264 | case Singular: 265 | // Merge global uninflected with singularsUninflected 266 | rules[r].Uninflected = merge(uninflected, uninflectedSingulars) 267 | } 268 | 269 | // Set InflectorRule.compiledUninflected by joining InflectorRule.Uninflected into 270 | // a single string then compile it. 271 | reString = fmt.Sprintf(`(?i)(^(?:%s))$`, strings.Join(rules[r].Uninflected, `|`)) 272 | rules[r].compiledUninflected = regexp.MustCompile(reString) 273 | 274 | // Prepare irregularMaps 275 | irregularMaps[r] = make(cache, len(rules[r].Irregular)) 276 | 277 | // Set InflectorRule.compiledIrregular by joining the irregularItem.word of Inflector.Irregular 278 | // into a single string then compile it. 279 | vIrregulars := make([]string, len(rules[r].Irregular)) 280 | for i, item := range rules[r].Irregular { 281 | vIrregulars[i] = item.word 282 | irregularMaps[r][item.word] = item.replacement 283 | } 284 | reString = fmt.Sprintf(`(?i)(.*)\b((?:%s))$`, strings.Join(vIrregulars, `|`)) 285 | rules[r].compiledIrregular = regexp.MustCompile(reString) 286 | 287 | // Compile all patterns in InflectorRule.Rules 288 | rules[r].compiledRules = make([]*compiledRule, len(rules[r].Rules)) 289 | for i, item := range rules[r].Rules { 290 | rules[r].compiledRules[i] = &compiledRule{item.replacement, regexp.MustCompile(item.pattern)} 291 | } 292 | 293 | // Prepare caches 294 | caches[r] = make(cache) 295 | 296 | return nil 297 | } 298 | 299 | // merge slice a and slice b 300 | func merge(a []string, b []string) []string { 301 | result := make([]string, len(a)+len(b)) 302 | copy(result, a) 303 | copy(result[len(a):], b) 304 | 305 | return result 306 | } 307 | 308 | // Pluralize returns string s in plural form. 309 | func Pluralize(s string) string { 310 | return getInflected(Plural, s) 311 | } 312 | 313 | // Singularize returns string s in singular form. 314 | func Singularize(s string) string { 315 | return getInflected(Singular, s) 316 | } 317 | 318 | func getInflected(r Rule, s string) string { 319 | mutex.Lock() 320 | defer mutex.Unlock() 321 | if v, ok := caches[r][s]; ok { 322 | return v 323 | } 324 | 325 | // Check for irregular words 326 | if res := rules[r].compiledIrregular.FindStringSubmatch(s); len(res) >= 3 { 327 | var buf bytes.Buffer 328 | 329 | buf.WriteString(res[1]) 330 | buf.WriteString(s[0:1]) 331 | buf.WriteString(irregularMaps[r][strings.ToLower(res[2])][1:]) 332 | 333 | // Cache it then returns 334 | caches[r][s] = buf.String() 335 | return caches[r][s] 336 | } 337 | 338 | // Check for uninflected words 339 | if rules[r].compiledUninflected.MatchString(s) { 340 | caches[r][s] = s 341 | return caches[r][s] 342 | } 343 | 344 | // Check each rule 345 | for _, re := range rules[r].compiledRules { 346 | if re.MatchString(s) { 347 | caches[r][s] = re.ReplaceAllString(s, re.replacement) 348 | return caches[r][s] 349 | } 350 | } 351 | 352 | // Returns unaltered 353 | caches[r][s] = s 354 | return caches[r][s] 355 | } 356 | --------------------------------------------------------------------------------