├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── Makefile ├── README.md ├── cmd └── pluralize │ └── main.go ├── go.mod ├── go.sum ├── pkg ├── tflags │ └── tflags.go └── version │ └── version.go ├── pluralize.go └── pluralize_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Log files 11 | *.log 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # exclude binery files 17 | pdx 18 | debug 19 | 20 | # exclude directories 21 | bin/ 22 | release/ 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.14.3 4 | cache: 5 | directories: 6 | - "$HOME/local" 7 | script: 8 | - make 9 | before_install: 10 | install: 11 | before_deploy: 12 | - make VERSION=${TRAVIS_TAG} release -j3 13 | - ls release/ 14 | deploy: 15 | provider: releases 16 | api_key: 17 | secure: 5tGm98IBWwiIwsZSRFNWSSP85IHAZrysGu1Z5od2N9n7crf1Q2RmNLLaDd9VMJc9zd9QDQb68BJBe8W+XT4XKAtcE6HRrgDveV5JpBIdAYQOqN9CBqEb0WEjYJeypoRzDTBfIayJnaZs3uNMcJm9e063ezhwOevflxbBOkBgVycVjA5kW8da5lVj0HnbTraUsIPjkpFDwyqgaVxCIKTe9eUwEUbKHBW8Sfp1b6sYLLiZrLyvAMvorMcnTdo0UfymwXxXlVOiDhTbiF6zGlW7aZ9uhxJRay3GIfFgqz8qcK1GIfG3HKl3wDtcWIGr3lNEsGTeBVr+55LD1gb+sCbMz3+mH4xK6d+LVuACmdHei/34WhXy0JXsIXthYnH/VbGLgELlL6UObat5Kjo7Db7Je5WcJuJ9GBGZ9o1MGfFG835QIYc3TMrFbnXrvAC7iefNKs/jr4Bdgapt7N0h9B/Ra5VUxZBmPveOC4tk3Km3j2ORau3rO4QvXTSuvvYXMLDENNwQlaDT5IFlAANLmcWY1vN16UoEXdPnOQyE90nsvpIQYnFGqLxgEJYJmp1br+U5ldKHIvnYke6h/f6FGDPsbIspxj6jSoITz7jHh3m8vEMuIl4pd1sreC5eQULJNZIZw786MVES2CSxsBsLB1nRZfE+uQu8B1vPRfnFvNBZ83Q= 18 | file_glob: true 19 | file: release/**/*.zip 20 | skip_cleanup: true 21 | overwrite: true 22 | on: 23 | tags: true 24 | notifications: 25 | email: false 26 | env: 27 | global: 28 | - PATH=$PATH:$HOME/local/bin 29 | - GO111MODULE=on 30 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${fileDirname}", 13 | "env": {}, 14 | "args": [ 15 | "-cmd", "Plural", 16 | "-word", "Alumnus" 17 | ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "make", 9 | "args": [ 10 | "build" 11 | ], 12 | "type": "shell", 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | } 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Gert Drapers 4 | 5 | Copyright (c) 2013 Blake Embrey (hello@blakeembrey.com) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := $(shell which bash) 2 | 3 | ## BOF define block 4 | 5 | BINARIES := pluralize 6 | BINARY = $(word 1, $@) 7 | 8 | PLATFORMS := windows linux darwin 9 | PLATFORM = $(word 1, $@) 10 | 11 | ROOT_DIR := $(shell git rev-parse --show-toplevel) 12 | BIN_DIR := $(ROOT_DIR)/bin 13 | REL_DIR := $(ROOT_DIR)/release 14 | SRC_DIR := $(ROOT_DIR)/cmd 15 | INC_DIR := $(ROOT_DIR)/include 16 | TMP_DIR := $(ROOT_DIR)/tmp 17 | 18 | VERSION :=`git describe --tags 2>/dev/null` 19 | COMMIT :=`git rev-parse --short HEAD 2>/dev/null` 20 | DATE :=`date "+%FT%T%z"` 21 | 22 | LDBASE := github.com/gertd/go-pluralize/pkg/version 23 | LDFLAGS := -ldflags "-w -s -X $(LDBASE).ver=${VERSION} -X $(LDBASE).date=${DATE} -X $(LDBASE).commit=${COMMIT}" 24 | 25 | GOARCH ?= amd64 26 | GOOS ?= $(shell go env GOOS) 27 | 28 | LINTER := $(BIN_DIR)/golangci-lint 29 | LINTVERSION:= v1.27.0 30 | 31 | TESTRUNNER := $(BIN_DIR)/gotestsum 32 | TESTVERSION:= v0.5.0 33 | 34 | PROTOC := $(BIN_DIR)/protoc 35 | PROTOCVER := 3.12.3 36 | 37 | NO_COLOR :=\033[0m 38 | OK_COLOR :=\033[32;01m 39 | ERR_COLOR :=\033[31;01m 40 | WARN_COLOR :=\033[36;01m 41 | ATTN_COLOR :=\033[33;01m 42 | 43 | ## EOF define block 44 | 45 | .PHONY: all 46 | all: deps gen build test lint 47 | 48 | deps: 49 | @echo -e "$(ATTN_COLOR)==> $@ $(NO_COLOR)" 50 | @GO111MODULE=on go mod download 51 | 52 | .PHONY: gen 53 | gen: deps $(BIN_DIR) 54 | @echo -e "$(ATTN_COLOR)==> $@ $(NO_COLOR)" 55 | @go generate ./... 56 | 57 | .PHONY: dobuild 58 | dobuild: 59 | @echo -e "$(ATTN_COLOR)==> $@ $(B) GOOS=$(P) GOARCH=$(GOARCH) VERSION=$(VERSION) COMMIT=$(COMMIT) DATE=$(DATE) $(NO_COLOR)" 60 | @GOOS=$(P) GOARCH=$(GOARCH) GO111MODULE=on go build $(LDFLAGS) -o $(T)/$(P)-$(GOARCH)/$(B)$(if $(findstring $(P),windows),".exe","") $(SRC_DIR)/$(B) 61 | ifneq ($(P),windows) 62 | @chmod +x $(T)/$(P)-$(GOARCH)/$(B) 63 | endif 64 | 65 | .PHONY: build 66 | build: $(BIN_DIR) deps 67 | @echo -e "$(ATTN_COLOR)==> $@ $(NO_COLOR)" 68 | @for b in ${BINARIES}; \ 69 | do \ 70 | $(MAKE) dobuild B=$${b} P=${GOOS} T=${BIN_DIR}; \ 71 | done 72 | 73 | .PHONY: doinstall 74 | doinstall: 75 | @echo -e "$(ATTN_COLOR)==> $@ $(B) GOOS=$(P) GOARCH=$(GOARCH) VERSION=$(VERSION) COMMIT=$(COMMIT) DATE=$(DATE) $(NO_COLOR)" 76 | @GOOS=$(P) GOARCH=$(GOARCH) GO111MODULE=on go install $(LDFLAGS) $(SRC_DIR)/$(B) 77 | 78 | .PHONY: install 79 | install: 80 | @echo -e "$(ATTN_COLOR)==> $@ $(NO_COLOR)" 81 | @for b in ${BINARIES}; \ 82 | do \ 83 | $(MAKE) doinstall B=$${b} P=${GOOS}; \ 84 | done 85 | 86 | .PHONY: dorelease 87 | dorelease: 88 | @echo -e "$(ATTN_COLOR)==> $@ build GOOS=$(P) GOARCH=$(GOARCH) VERSION=$(VERSION) COMMIT=$(COMMIT) DATE=$(DATE) $(NO_COLOR)" 89 | @GOOS=$(P) GOARCH=$(GOARCH) GO111MODULE=on go build $(LDFLAGS) -o $(T)/$(P)-$(GOARCH)/$(B)$(if $(findstring $(P),windows),".exe","") $(SRC_DIR)/$(B) 90 | ifneq ($(P),windows) 91 | @chmod +x $(T)/$(P)-$(GOARCH)/$(B) 92 | endif 93 | @echo -e "$(ATTN_COLOR)==> $@ zip $(B)-$(P)-$(GOARCH).zip $(NO_COLOR)" 94 | @zip -j $(T)/$(P)-$(GOARCH)/$(B)-$(P)-$(GOARCH).zip $(T)/$(P)-$(GOARCH)/$(B)$(if $(findstring $(P),windows),".exe","") >/dev/null 95 | 96 | .PHONY: release 97 | release: $(REL_DIR) 98 | @echo -e "$(ATTN_COLOR)==> $@ $(NO_COLOR)" 99 | @for b in ${BINARIES}; \ 100 | do \ 101 | for p in ${PLATFORMS}; \ 102 | do \ 103 | $(MAKE) dorelease B=$${b} P=$${p} T=${REL_DIR}; \ 104 | done; \ 105 | done \ 106 | 107 | $(TESTRUNNER): 108 | @echo -e "$(ATTN_COLOR)==> get $@ $(NO_COLOR)" 109 | @GOBIN=$(BIN_DIR) go get -u gotest.tools/gotestsum 110 | 111 | .PHONY: test 112 | test: $(TESTRUNNER) 113 | @echo -e "$(ATTN_COLOR)==> $@ $(NO_COLOR)" 114 | @CGO_ENABLED=0 $(BIN_DIR)/gotestsum --format short-verbose -- -count=1 -v $(ROOT_DIR)/... 115 | 116 | $(LINTER): 117 | @echo -e "$(ATTN_COLOR)==> get $@ $(NO_COLOR)" 118 | @curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s $(LINTVERSION) 119 | 120 | .PHONY: lint 121 | lint: $(LINTER) 122 | @echo -e "$(ATTN_COLOR)==> $@ $(NO_COLOR)" 123 | @CGO_ENABLED=0 $(LINTER) run --enable-all 124 | @echo -e "$(NO_COLOR)\c" 125 | 126 | .PHONY: doclean 127 | doclean: 128 | @echo -e "$(ATTN_COLOR)==> $@ $(B) GOOS=$(P) $(NO_COLOR)" 129 | @if [ -a $(GOPATH)/bin/$(B)$(if $(findstring $(P),windows),".exe","") ];\ 130 | then \ 131 | rm $(GOPATH)/bin/$(B)$(if $(findstring $(P),windows),".exe",""); \ 132 | fi 133 | 134 | .PHONY: clean 135 | clean: 136 | @echo -e "$(ATTN_COLOR)==> $@ $(NO_COLOR)" 137 | @rm -rf $(BIN_DIR) 138 | @rm -rf $(REL_DIR) 139 | @go clean 140 | @for b in ${BINARIES}; \ 141 | do \ 142 | $(MAKE) doclean B=$${b} P=${GOOS}; \ 143 | done 144 | 145 | $(REL_DIR): 146 | @echo -e "$(ATTN_COLOR)==> create REL_DIR $(REL_DIR) $(NO_COLOR)" 147 | @mkdir -p $(REL_DIR) 148 | 149 | $(BIN_DIR): 150 | @echo -e "$(ATTN_COLOR)==> create BIN_DIR $(BIN_DIR) $(NO_COLOR)" 151 | @mkdir -p $(BIN_DIR) 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-pluralize 2 | [![Build Status](https://travis-ci.org/gertd/go-pluralize.svg?branch=master)](https://travis-ci.org/gertd/go-pluralize) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/gertd/go-pluralize)](https://goreportcard.com/report/github.com/gertd/go-pluralize) 4 | [![GoDoc](https://godoc.org/github.com/gertd/go-pluralize?status.svg)](https://godoc.org/github.com/gertd/go-pluralize) 5 | 6 | Pluralize and singularize any word 7 | 8 | # Acknowledgements 9 | > The go-pluralize module is the Golang adaptation of the great work from [Blake Embrey](https://www.npmjs.com/~blakeembrey) and other contributors who created and maintain the NPM JavaScript [pluralize](https://www.npmjs.com/package/pluralize) package. 10 | > The originating Javascript implementation can be found on https://github.com/blakeembrey/pluralize 11 | > 12 | > Without their great work this module would have taken a lot more effort, **thank you all**! 13 | 14 | # Version mapping 15 | 16 | The latest go-pluralize version is compatible with [pluralize](https://www.npmjs.com/package/pluralize) version 8.0.0 commit [#36f03cd](https://github.com/blakeembrey/pluralize/commit/36f03cd2d573fa6d23e12e1529fa4627e2af74b4) 17 | 18 | | go-pluralize version | NPM Pluralize Package version | 19 | | ------------- | ------------- | 20 | | 0.2.0 - Jan 25, 2022 [v0.2.0](https://github.com/gertd/go-pluralize/releases/tag/v0.2.0) | 8.0.0 - Oct 6, 2021 [#36f03cd](https://github.com/blakeembrey/pluralize/commit/36f03cd2d573fa6d23e12e1529fa4627e2af74b4) 21 | | 0.1.7 - Jun 23, 2020 [v0.1.7](https://github.com/gertd/go-pluralize/releases/tag/v0.1.7) | 8.0.0 - Mar 14, 2020 [#e507706](https://github.com/blakeembrey/pluralize/commit/e507706be779612c06ebfd6043163e063e791d79) 22 | | 0.1.2 - Apr 1, 2020 [v0.1.2](https://github.com/gertd/go-pluralize/releases/tag/v0.1.2) | 8.0.0 - Mar 14, 2020 [#e507706](https://github.com/blakeembrey/pluralize/commit/e507706be779612c06ebfd6043163e063e791d79) 23 | | 0.1.1 - Sep 15, 2019 [v0.1.1](https://github.com/gertd/go-pluralize/releases/tag/v0.1.1) | 8.0.0 - Aug 27, 2019 [#abb3991](https://github.com/blakeembrey/pluralize/commit/abb399111aedd1d62dd418d7e0217d85f5bf22c9) 24 | | 0.1.0 - Jun 12, 2019 [v0.1.0](https://github.com/gertd/go-pluralize/releases/tag/v0.1.0) | 8.0.0 - May 24, 2019 [#0265e4d](https://github.com/blakeembrey/pluralize/commit/0265e4d131ecad8e11c420fa4be98b75dc92c33d) 25 | 26 | # Installation 27 | 28 | To install the go module: 29 | 30 | go get -u github.com/gertd/go-pluralize 31 | 32 | To lock down a specific the version: 33 | 34 | go get -u github.com/gertd/go-pluralize@v0.2.0 35 | 36 | Download the sources and binaries from the latest [release](https://github.com/gertd/go-pluralize/releases/latest) 37 | 38 | 39 | # Usage 40 | 41 | ## Code 42 | import pluralize "github.com/gertd/go-pluralize" 43 | 44 | word := "Empire" 45 | 46 | pluralize := pluralize.NewClient() 47 | 48 | fmt.Printf("IsPlural(%s) => %t\n", input, pluralize.IsPlural(word)) 49 | fmt.Printf("IsSingular(%s) => %t\n", input, pluralize.IsSingular(word)) 50 | fmt.Printf("Plural(%s) => %s\n", input, pluralize.Plural(word)) 51 | fmt.Printf("Singular(%s) => %s\n", input, pluralize.Singular(word)) 52 | 53 | ## Result 54 | IsPlural(Empire) => false 55 | IsSingular(Empire) => true 56 | Plural(Empire) => Empires 57 | Singular(Empire) => Empire 58 | 59 | 60 | # Pluralize Command Line 61 | 62 | ## Installation 63 | go get -x github.com/gertd/go-pluralize/cmd/pluralize 64 | 65 | 66 | 67 | 68 | ## Usage 69 | 70 | ### Help 71 | pluralize -help 72 | Usage of ./bin/pluralize: 73 | -cmd string 74 | command [All|IsPlural|IsSingular|Plural|Singular] (default "All") 75 | -version 76 | display version info 77 | -word string 78 | input value 79 | 80 | ### Word with All Commands 81 | pluralize -word Empire 82 | 83 | IsPlural(Empire) => false 84 | IsSingular(Empire) => true 85 | Plural(Empire) => Empires 86 | Singular(Empire) => Empire 87 | 88 | ### Is Word Plural? 89 | pluralize -word Cactus -cmd IsPlural 90 | 91 | IsPlural(Cactus) => false 92 | 93 | ### Is Word Singular? 94 | pluralize -word Cacti -cmd IsSingular 95 | 96 | IsSingular(Cacti) => false 97 | 98 | ### Word Make Plural 99 | pluralize -word Cactus -cmd Plural 100 | 101 | Plural(Cactus) => Cacti 102 | 103 | ### Word Make Singular 104 | pluralize -word Cacti -cmd Singular 105 | 106 | Singular(Cacti) => Cactus 107 | -------------------------------------------------------------------------------- /cmd/pluralize/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/gertd/go-pluralize" 9 | "github.com/gertd/go-pluralize/pkg/tflags" 10 | "github.com/gertd/go-pluralize/pkg/version" 11 | ) 12 | 13 | const ( 14 | appName = "pluralize" 15 | ) 16 | 17 | func main() { 18 | var ( 19 | word = flag.String("word", "", "input value") 20 | cmd = flag.String("cmd", "All", "command [All|IsPlural|IsSingular|Plural|Singular]") 21 | showVersion = flag.Bool("version", false, "display version info") 22 | ) 23 | 24 | flag.Parse() 25 | 26 | if showVersion != nil && *showVersion { 27 | displayVersionInfo(appName) 28 | return 29 | } 30 | 31 | if word == nil || len(*word) == 0 { 32 | fmt.Printf("-word not specified\n") 33 | return 34 | } 35 | 36 | pluralize := pluralize.NewClient() 37 | 38 | testCmd := tflags.TestCmdString(*cmd) 39 | if testCmd.Has(tflags.TestCmdUnknown) { 40 | fmt.Printf("Unknown -cmd value\nOptions: [All|IsPlural|IsSingular|Plural|Singular]\n") 41 | return 42 | } 43 | 44 | if testCmd.Has(tflags.TestCmdIsPlural) { 45 | fmt.Printf("IsPlural(%s) => %t\n", *word, pluralize.IsPlural(*word)) 46 | } 47 | 48 | if testCmd.Has(tflags.TestCmdIsSingular) { 49 | fmt.Printf("IsSingular(%s) => %t\n", *word, pluralize.IsSingular(*word)) 50 | } 51 | 52 | if testCmd.Has(tflags.TestCmdPlural) { 53 | fmt.Printf("Plural(%s) => %s\n", *word, pluralize.Plural(*word)) 54 | } 55 | 56 | if testCmd.Has(tflags.TestCmdSingular) { 57 | fmt.Printf("Singular(%s) => %s\n", *word, pluralize.Singular(*word)) 58 | } 59 | } 60 | 61 | func displayVersionInfo(name string) { 62 | fmt.Fprintf(os.Stdout, "%s - %s\n", 63 | name, 64 | version.GetInfo(), 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gertd/go-pluralize 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gertd/go-pluralize/8c94b92eb8ba076775a4c86b8d835783b4d391c4/go.sum -------------------------------------------------------------------------------- /pkg/tflags/tflags.go: -------------------------------------------------------------------------------- 1 | package tflags 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // TestCmd -- enum. 8 | type TestCmd uint8 9 | 10 | // TestCmd -- enum constants. 11 | const ( 12 | TestCmdUnknown TestCmd = 1 << iota 13 | TestCmdIsPlural 14 | TestCmdIsSingular 15 | TestCmdPlural 16 | TestCmdSingular 17 | TestCmdAll = TestCmdIsPlural + TestCmdIsSingular + TestCmdPlural + TestCmdSingular 18 | ) 19 | 20 | // TestCmd -- string constants. 21 | const ( 22 | testCmdUnknown = "Unknown" 23 | testCmdIsPlural = "IsPlural" 24 | testCmdIsSingular = "IsSingular" 25 | testCmdPlural = "Plural" 26 | testCmdSingular = "Singular" 27 | testCmdAll = "All" 28 | ) 29 | 30 | // testCmdID -- map enum to string. 31 | func testCmdID(t TestCmd) string { 32 | innerTestCmdID := map[TestCmd]string{ 33 | 34 | TestCmdUnknown: testCmdUnknown, 35 | TestCmdIsPlural: testCmdIsPlural, 36 | TestCmdIsSingular: testCmdIsSingular, 37 | TestCmdPlural: testCmdPlural, 38 | TestCmdSingular: testCmdSingular, 39 | TestCmdAll: testCmdAll, 40 | } 41 | 42 | return innerTestCmdID[t] 43 | } 44 | 45 | // testCmdName -- map string to enum 46 | // func testCmdName() map[string]TestCmd { 47 | 48 | // return map[string]TestCmd{ 49 | // strings.ToLower(testCmdUnknown): TestCmdUnknown, 50 | // strings.ToLower(testCmdIsPlural): TestCmdIsPlural, 51 | // strings.ToLower(testCmdIsSingular): TestCmdIsSingular, 52 | // strings.ToLower(testCmdPlural): TestCmdPlural, 53 | // strings.ToLower(testCmdSingular): TestCmdSingular, 54 | // strings.ToLower(testCmdAll): TestCmdAll, 55 | // } 56 | // } 57 | 58 | // testCmdName -- map string to enum value. 59 | func testCmdName(s string) TestCmd { 60 | f := func() func(s string) TestCmd { 61 | innerTestCmdName := map[string]TestCmd{ 62 | strings.ToLower(testCmdUnknown): TestCmdUnknown, 63 | strings.ToLower(testCmdIsPlural): TestCmdIsPlural, 64 | strings.ToLower(testCmdIsSingular): TestCmdIsSingular, 65 | strings.ToLower(testCmdPlural): TestCmdPlural, 66 | strings.ToLower(testCmdSingular): TestCmdSingular, 67 | strings.ToLower(testCmdAll): TestCmdAll, 68 | } 69 | 70 | inner := func(s2 string) TestCmd { 71 | if value, ok := innerTestCmdName[strings.ToLower(s2)]; ok { 72 | return value 73 | } 74 | 75 | return TestCmdUnknown 76 | } 77 | 78 | return inner 79 | } 80 | 81 | return f()(s) 82 | } 83 | 84 | // String -- stringify TestCmd. 85 | func (t TestCmd) String() string { 86 | return testCmdID(t) 87 | } 88 | 89 | // Set -- set flag. 90 | func (t *TestCmd) Set(flag TestCmd) { 91 | *t |= flag 92 | } 93 | 94 | // Clear -- clear flag. 95 | func (t *TestCmd) Clear(flag TestCmd) { 96 | *t &^= flag 97 | } 98 | 99 | // Toggle -- toggle flag state. 100 | func (t *TestCmd) Toggle(flag TestCmd) { 101 | *t ^= flag 102 | } 103 | 104 | // Has -- is flag set?. 105 | func (t TestCmd) Has(flag TestCmd) bool { 106 | return t&flag != 0 107 | } 108 | 109 | // TestCmdString -- convert string reprensentation in to enum value. 110 | func TestCmdString(s string) TestCmd { 111 | return testCmdName(s) 112 | } 113 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | var ( 9 | ver string //nolint:gochecknoglobals 10 | date string //nolint:gochecknoglobals 11 | commit string //nolint:gochecknoglobals 12 | ) 13 | 14 | // Info - version info. 15 | type Info struct { 16 | Version string 17 | Date string 18 | Commit string 19 | } 20 | 21 | // GetInfo - get version stamp information. 22 | func GetInfo() Info { 23 | return Info{ 24 | Version: ver, 25 | Date: date, 26 | Commit: commit, 27 | } 28 | } 29 | 30 | // String() -- return version info string. 31 | func (vi Info) String() string { 32 | return fmt.Sprintf("%s #%s-%s-%s [%s]", 33 | vi.Version, 34 | vi.Commit, 35 | runtime.GOOS, 36 | runtime.GOARCH, 37 | vi.Date, 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /pluralize.go: -------------------------------------------------------------------------------- 1 | package pluralize 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // Rule -- pluralize rule expression and replacement value. 11 | type Rule struct { 12 | expression *regexp.Regexp 13 | replacement string 14 | } 15 | 16 | // Client -- pluralize client. 17 | type Client struct { 18 | pluralRules []Rule 19 | singularRules []Rule 20 | uncountables map[string]bool 21 | irregularSingles map[string]string 22 | irregularPlurals map[string]string 23 | interpolateExpr *regexp.Regexp 24 | } 25 | 26 | // NewClient - pluralization client factory method. 27 | func NewClient() *Client { 28 | client := Client{} 29 | client.init() 30 | 31 | return &client 32 | } 33 | 34 | func (c *Client) init() { 35 | c.pluralRules = make([]Rule, 0) 36 | c.singularRules = make([]Rule, 0) 37 | c.uncountables = make(map[string]bool) 38 | c.irregularSingles = make(map[string]string) 39 | c.irregularPlurals = make(map[string]string) 40 | 41 | c.loadIrregularRules() 42 | c.loadPluralizationRules() 43 | c.loadSingularizationRules() 44 | c.loadUncountableRules() 45 | c.interpolateExpr = regexp.MustCompile(`\$(\d{1,2})`) 46 | } 47 | 48 | // Pluralize -- Pluralize or singularize a word based on the passed in count. 49 | // word: the word to pluralize 50 | // count: how many of the word exist 51 | // inclusive: whether to prefix with the number (e.g. 3 ducks) 52 | func (c *Client) Pluralize(word string, count int, inclusive bool) string { 53 | pluralized := func() func(string) string { 54 | if count == 1 { 55 | return c.Singular 56 | } 57 | 58 | return c.Plural 59 | } 60 | 61 | if inclusive { 62 | return fmt.Sprintf("%d %s", count, pluralized()(word)) 63 | } 64 | 65 | return pluralized()(word) 66 | } 67 | 68 | // Plural -- Pluralize a word. 69 | func (c *Client) Plural(word string) string { 70 | return c.replaceWord(c.irregularSingles, c.irregularPlurals, c.pluralRules)(word) 71 | } 72 | 73 | // IsPlural -- Check if a word is plural. 74 | func (c *Client) IsPlural(word string) bool { 75 | return c.checkWord(c.irregularSingles, c.irregularPlurals, c.pluralRules)(word) 76 | } 77 | 78 | // Singular -- Singularize a word. 79 | func (c *Client) Singular(word string) string { 80 | return c.replaceWord(c.irregularPlurals, c.irregularSingles, c.singularRules)(word) 81 | } 82 | 83 | // IsSingular -- Check if a word is singular. 84 | func (c *Client) IsSingular(word string) bool { 85 | return c.checkWord(c.irregularPlurals, c.irregularSingles, c.singularRules)(word) 86 | } 87 | 88 | // AddPluralRule -- Add a pluralization rule to the collection. 89 | func (c *Client) AddPluralRule(rule string, replacement string) { 90 | c.pluralRules = append(c.pluralRules, Rule{sanitizeRule(rule), replacement}) 91 | } 92 | 93 | // AddSingularRule -- Add a singularization rule to the collection. 94 | func (c *Client) AddSingularRule(rule string, replacement string) { 95 | c.singularRules = append(c.singularRules, Rule{sanitizeRule(rule), replacement}) 96 | } 97 | 98 | // AddUncountableRule -- Add an uncountable word rule. 99 | func (c *Client) AddUncountableRule(word string) { 100 | if !isExpr(word) { 101 | c.uncountables[strings.ToLower(word)] = true 102 | return 103 | } 104 | 105 | c.AddPluralRule(word, `$0`) 106 | c.AddSingularRule(word, `$0`) 107 | } 108 | 109 | // AddIrregularRule -- Add an irregular word definition. 110 | func (c *Client) AddIrregularRule(single string, plural string) { 111 | p := strings.ToLower(plural) 112 | s := strings.ToLower(single) 113 | 114 | c.irregularSingles[s] = p 115 | c.irregularPlurals[p] = s 116 | } 117 | 118 | func (c *Client) replaceWord(replaceMap map[string]string, keepMap map[string]string, rules []Rule) func(w string) string { //nolint:lll 119 | f := func(word string) string { 120 | // Get the correct token and case restoration functions. 121 | var token = strings.ToLower(word) 122 | 123 | // Check against the keep object map. 124 | if _, ok := keepMap[token]; ok { 125 | return restoreCase(word, token) 126 | } 127 | 128 | // Check against the replacement map for a direct word replacement. 129 | if replaceToken, ok := replaceMap[token]; ok { 130 | return restoreCase(word, replaceToken) 131 | } 132 | 133 | // Run all the rules against the word. 134 | return c.sanitizeWord(token, word, rules) 135 | } 136 | 137 | return f 138 | } 139 | 140 | func (c *Client) checkWord(replaceMap map[string]string, keepMap map[string]string, rules []Rule) func(w string) bool { 141 | f := func(word string) bool { 142 | var token = strings.ToLower(word) 143 | 144 | if _, ok := keepMap[token]; ok { 145 | return true 146 | } 147 | 148 | if _, ok := replaceMap[token]; ok { 149 | return false 150 | } 151 | 152 | return c.sanitizeWord(token, token, rules) == token 153 | } 154 | 155 | return f 156 | } 157 | 158 | func (c *Client) interpolate(str string, args []string) string { 159 | lookup := map[string]string{} 160 | 161 | for _, submatch := range c.interpolateExpr.FindAllStringSubmatch(str, -1) { 162 | element, _ := strconv.Atoi(submatch[1]) 163 | lookup[submatch[0]] = args[element] 164 | } 165 | 166 | result := c.interpolateExpr.ReplaceAllStringFunc(str, func(repl string) string { 167 | return lookup[repl] 168 | }) 169 | 170 | return result 171 | } 172 | 173 | func (c *Client) replace(word string, rule Rule) string { 174 | return rule.expression.ReplaceAllStringFunc(word, func(w string) string { 175 | match := rule.expression.FindString(word) 176 | index := rule.expression.FindStringIndex(word)[0] 177 | args := rule.expression.FindAllStringSubmatch(word, -1)[0] 178 | 179 | result := c.interpolate(rule.replacement, args) 180 | 181 | if match == `` { 182 | return restoreCase(word[index-1:index], result) 183 | } 184 | return restoreCase(match, result) 185 | }) 186 | } 187 | 188 | func (c *Client) sanitizeWord(token string, word string, rules []Rule) string { 189 | // If empty string 190 | if len(token) == 0 { 191 | return word 192 | } 193 | // If does not need fixup 194 | if _, ok := c.uncountables[token]; ok { 195 | return word 196 | } 197 | 198 | // Iterate over the sanitization rules and use the first one to match. 199 | // NOTE: iterate rules array in reverse order specific => general rules 200 | for i := len(rules) - 1; i >= 0; i-- { 201 | if rules[i].expression.MatchString(word) { 202 | return c.replace(word, rules[i]) 203 | } 204 | } 205 | 206 | return word 207 | } 208 | 209 | func sanitizeRule(rule string) *regexp.Regexp { 210 | if isExpr(rule) { 211 | return regexp.MustCompile(rule) 212 | } 213 | 214 | return regexp.MustCompile(`(?i)^` + rule + `$`) 215 | } 216 | 217 | func restoreCase(word string, token string) string { 218 | // Tokens are an exact match. 219 | if word == token { 220 | return token 221 | } 222 | 223 | // Lower cased words. E.g. "hello". 224 | if word == strings.ToLower(word) { 225 | return strings.ToLower(token) 226 | } 227 | 228 | // Upper cased words. E.g. "WHISKY". 229 | if word == strings.ToUpper(word) { 230 | return strings.ToUpper(token) 231 | } 232 | 233 | // Title cased words. E.g. "Title". 234 | if word[:1] == strings.ToUpper(word[:1]) { 235 | return strings.ToUpper(token[:1]) + strings.ToLower(token[1:]) 236 | } 237 | 238 | // Lower cased words. E.g. "test". 239 | return strings.ToLower(token) 240 | } 241 | 242 | // isExpr -- helper to detect if string represents an expression by checking first character to be `(`. 243 | func isExpr(s string) bool { 244 | return s[:1] == `(` 245 | } 246 | 247 | func (c *Client) loadIrregularRules() { //nolint:funlen 248 | var irregularRules = []struct { 249 | single string 250 | plural string 251 | }{ 252 | // Pronouns. 253 | {`I`, `we`}, 254 | {`me`, `us`}, 255 | {`he`, `they`}, 256 | {`she`, `they`}, 257 | {`them`, `them`}, 258 | {`myself`, `ourselves`}, 259 | {`yourself`, `yourselves`}, 260 | {`itself`, `themselves`}, 261 | {`herself`, `themselves`}, 262 | {`himself`, `themselves`}, 263 | {`themself`, `themselves`}, 264 | {`is`, `are`}, 265 | {`was`, `were`}, 266 | {`has`, `have`}, 267 | {`this`, `these`}, 268 | {`that`, `those`}, 269 | {`my`, `our`}, 270 | {`its`, `their`}, 271 | {`his`, `their`}, 272 | {`her`, `their`}, 273 | // Words ending in with a consonant and `o`. 274 | {`echo`, `echoes`}, 275 | {`dingo`, `dingoes`}, 276 | {`volcano`, `volcanoes`}, 277 | {`tornado`, `tornadoes`}, 278 | {`torpedo`, `torpedoes`}, 279 | // Ends with `us`. 280 | {`genus`, `genera`}, 281 | {`viscus`, `viscera`}, 282 | // Ends with `ma`. 283 | {`stigma`, `stigmata`}, 284 | {`stoma`, `stomata`}, 285 | {`dogma`, `dogmata`}, 286 | {`lemma`, `lemmata`}, 287 | {`schema`, `schemata`}, 288 | {`anathema`, `anathemata`}, 289 | // Other irregular rules. 290 | {`ox`, `oxen`}, 291 | {`axe`, `axes`}, 292 | {`die`, `dice`}, 293 | {`yes`, `yeses`}, 294 | {`foot`, `feet`}, 295 | {`eave`, `eaves`}, 296 | {`goose`, `geese`}, 297 | {`tooth`, `teeth`}, 298 | {`quiz`, `quizzes`}, 299 | {`human`, `humans`}, 300 | {`proof`, `proofs`}, 301 | {`carve`, `carves`}, 302 | {`valve`, `valves`}, 303 | {`looey`, `looies`}, 304 | {`thief`, `thieves`}, 305 | {`groove`, `grooves`}, 306 | {`pickaxe`, `pickaxes`}, 307 | {`passerby`, `passersby`}, 308 | {`canvas`, `canvases`}, 309 | {`sms`, `sms`}, 310 | } 311 | 312 | for _, r := range irregularRules { 313 | c.AddIrregularRule(r.single, r.plural) 314 | } 315 | } 316 | 317 | func (c *Client) loadPluralizationRules() { 318 | var pluralizationRules = []struct { 319 | rule string 320 | replacement string 321 | }{ 322 | {`(?i)s?$`, `s`}, 323 | {`(?i)[^[:ascii:]]$`, `$0`}, 324 | {`(?i)([^aeiou]ese)$`, `$1`}, 325 | {`(?i)(ax|test)is$`, `$1es`}, 326 | {`(?i)(alias|[^aou]us|t[lm]as|gas|ris)$`, `$1es`}, 327 | {`(?i)(e[mn]u)s?$`, `$1s`}, 328 | {`(?i)([^l]ias|[aeiou]las|[ejzr]as|[iu]am)$`, `$1`}, 329 | {`(?i)(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$`, `$1i`}, //nolint:lll,misspell 330 | {`(?i)(alumn|alg|vertebr)(?:a|ae)$`, `$1ae`}, 331 | {`(?i)(seraph|cherub)(?:im)?$`, `$1im`}, 332 | {`(?i)(her|at|gr)o$`, `$1oes`}, 333 | {`(?i)(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|automat|quor)(?:a|um)$`, `$1a`}, //nolint:lll,misspell 334 | {`(?i)(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)(?:a|on)$`, `$1a`}, 335 | {`(?i)sis$`, `ses`}, 336 | {`(?i)(?:(kni|wi|li)fe|(ar|l|ea|eo|oa|hoo)f)$`, `$1$2ves`}, 337 | {`(?i)([^aeiouy]|qu)y$`, `$1ies`}, 338 | {`(?i)([^ch][ieo][ln])ey$`, `$1ies`}, 339 | {`(?i)(x|ch|ss|sh|zz)$`, `$1es`}, 340 | {`(?i)(matr|cod|mur|sil|vert|ind|append)(?:ix|ex)$`, `$1ices`}, 341 | {`(?i)\b((?:tit)?m|l)(?:ice|ouse)$`, `$1ice`}, 342 | {`(?i)(pe)(?:rson|ople)$`, `$1ople`}, 343 | {`(?i)(child)(?:ren)?$`, `$1ren`}, 344 | {`(?i)eaux$`, `$0`}, 345 | {`(?i)m[ae]n$`, `men`}, 346 | {`thou`, `you`}, 347 | } 348 | 349 | for _, r := range pluralizationRules { 350 | c.AddPluralRule(r.rule, r.replacement) 351 | } 352 | } 353 | 354 | func (c *Client) loadSingularizationRules() { 355 | var singularizationRules = []struct { 356 | rule string 357 | replacement string 358 | }{ 359 | {`(?i)s$`, ``}, 360 | {`(?i)(ss)$`, `$1`}, 361 | {`(?i)(wi|kni|(?:after|half|high|low|mid|non|night|[^\w]|^)li)ves$`, `$1fe`}, 362 | {`(?i)(ar|(?:wo|[ae])l|[eo][ao])ves$`, `$1f`}, 363 | {`(?i)ies$`, `y`}, 364 | {`(?i)(dg|ss|ois|lk|ok|wn|mb|th|ch|ec|oal|is|ck|ix|sser|ts|wb)ies$`, `$1ie`}, 365 | {`(?i)\b(l|(?:neck|cross|hog|aun)?t|coll|faer|food|gen|goon|group|hipp|junk|vegg|(?:pork)?p|charl|calor|cut)ies$`, `$1ie`}, //nolint:lll 366 | {`(?i)\b(mon|smil)ies$`, `$1ey`}, 367 | {`(?i)\b((?:tit)?m|l)ice$`, `$1ouse`}, 368 | {`(?i)(seraph|cherub)im$`, `$1`}, 369 | {`(?i)(x|ch|ss|sh|zz|tto|go|cho|alias|[^aou]us|t[lm]as|gas|(?:her|at|gr)o|[aeiou]ris)(?:es)?$`, `$1`}, 370 | {`(?i)(analy|diagno|parenthe|progno|synop|the|empha|cri|ne)(?:sis|ses)$`, `$1sis`}, 371 | {`(?i)(movie|twelve|abuse|e[mn]u)s$`, `$1`}, 372 | {`(?i)(test)(?:is|es)$`, `$1is`}, 373 | {`(?i)(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$`, `$1us`}, //nolint:lll,misspell 374 | {`(?i)(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|quor)a$`, `$1um`}, //nolint:lll,misspell 375 | {`(?i)(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)a$`, `$1on`}, 376 | {`(?i)(alumn|alg|vertebr)ae$`, `$1a`}, 377 | {`(?i)(cod|mur|sil|vert|ind)ices$`, `$1ex`}, 378 | {`(?i)(matr|append)ices$`, `$1ix`}, 379 | {`(?i)(pe)(rson|ople)$`, `$1rson`}, 380 | {`(?i)(child)ren$`, `$1`}, 381 | {`(?i)(eau)x?$`, `$1`}, 382 | {`(?i)men$`, `man`}, 383 | } 384 | 385 | for _, r := range singularizationRules { 386 | c.AddSingularRule(r.rule, r.replacement) 387 | } 388 | } 389 | 390 | func (c *Client) loadUncountableRules() { //nolint:funlen 391 | var uncountableRules = []string{ 392 | // Singular words with no plurals. 393 | `adulthood`, 394 | `advice`, 395 | `agenda`, 396 | `aid`, 397 | `aircraft`, 398 | `alcohol`, 399 | `ammo`, 400 | `analytics`, 401 | `anime`, 402 | `athletics`, 403 | `audio`, 404 | `bison`, 405 | `blood`, 406 | `bream`, 407 | `buffalo`, 408 | `butter`, 409 | `carp`, 410 | `cash`, 411 | `chassis`, 412 | `chess`, 413 | `clothing`, 414 | `cod`, 415 | `commerce`, 416 | `cooperation`, 417 | `corps`, 418 | `debris`, 419 | `diabetes`, 420 | `digestion`, 421 | `elk`, 422 | `energy`, 423 | `equipment`, 424 | `excretion`, 425 | `expertise`, 426 | `firmware`, 427 | `flounder`, 428 | `fun`, 429 | `gallows`, 430 | `garbage`, 431 | `graffiti`, 432 | `hardware`, 433 | `headquarters`, 434 | `health`, 435 | `herpes`, 436 | `highjinks`, 437 | `homework`, 438 | `housework`, 439 | `information`, 440 | `jeans`, 441 | `justice`, 442 | `kudos`, 443 | `labour`, 444 | `literature`, 445 | `machinery`, 446 | `mackerel`, 447 | `mail`, 448 | `media`, 449 | `mews`, 450 | `moose`, 451 | `music`, 452 | `mud`, 453 | `manga`, 454 | `news`, 455 | `only`, 456 | `personnel`, 457 | `pike`, 458 | `plankton`, 459 | `pliers`, 460 | `police`, 461 | `pollution`, 462 | `premises`, 463 | `rain`, 464 | `research`, 465 | `rice`, 466 | `salmon`, 467 | `scissors`, 468 | `series`, 469 | `sewage`, 470 | `shambles`, 471 | `shrimp`, 472 | `software`, 473 | `staff`, 474 | `swine`, 475 | `tennis`, 476 | `traffic`, 477 | `transportation`, 478 | `trout`, 479 | `tuna`, 480 | `wealth`, 481 | `welfare`, 482 | `whiting`, 483 | `wildebeest`, 484 | `wildlife`, 485 | `you`, 486 | // Regexes. 487 | `(?i)pok[eé]mon$`, // 488 | `(?i)[^aeiou]ese$`, // "chinese", "japanese" 489 | `(?i)deer$`, // "deer", "reindeer" 490 | `(?i)(fish)$`, // "fish", "blowfish", "angelfish" 491 | `(?i)measles$`, // 492 | `(?i)o[iu]s$`, // "carnivorous" 493 | `(?i)pox$`, // "chickpox", "smallpox" 494 | `(?i)sheep$`, // 495 | } 496 | 497 | for _, w := range uncountableRules { 498 | c.AddUncountableRule(w) 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /pluralize_test.go: -------------------------------------------------------------------------------- 1 | package pluralize //nolint:testpackage 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "github.com/gertd/go-pluralize/pkg/tflags" 10 | ) 11 | 12 | type TestEntry struct { 13 | input string 14 | expected string 15 | } 16 | 17 | type params struct { 18 | passLog *bool 19 | word *string 20 | cmd *string 21 | } 22 | 23 | var ( 24 | p params //nolint:gochecknoglobals 25 | ) 26 | 27 | func TestMain(m *testing.M) { 28 | p.passLog = flag.Bool("pass", false, "log PASS results") 29 | p.word = flag.String("word", "", "input value") 30 | p.cmd = flag.String("cmd", "all", "command name [optional]") 31 | 32 | flag.Parse() 33 | 34 | os.Exit(m.Run()) 35 | } 36 | 37 | // plogf -- PASSED result log. 38 | func plogf(t *testing.T, format string, a ...interface{}) { 39 | if *p.passLog { 40 | t.Logf(format, a...) 41 | } 42 | } 43 | 44 | // slog -- Summary result log. 45 | func slog(name string, passed int, failed int, total int) { 46 | fmt.Fprintf(os.Stdout, ">>> %s PASSED=%d FAILED=%d OF %d\n", 47 | name, passed, failed, total) 48 | } 49 | 50 | func TestCmd(t *testing.T) { 51 | if p.word == nil || len(*p.word) == 0 { 52 | t.SkipNow() 53 | return 54 | } 55 | 56 | testCmd := tflags.TestCmdString(*p.cmd) 57 | 58 | if testCmd.Has(tflags.TestCmdUnknown) { 59 | t.Error(fmt.Errorf("unknown -cmd value %s, valid [All|IsPlural|IsSingular|Plural|Singular]", *p.cmd)) 60 | return 61 | } 62 | 63 | pluralize := NewClient() 64 | 65 | if testCmd.Has(tflags.TestCmdIsPlural) { 66 | t.Logf("IsPlural(%s) => %t\n", *p.word, pluralize.IsPlural(*p.word)) 67 | } 68 | 69 | if testCmd.Has(tflags.TestCmdIsSingular) { 70 | t.Logf("IsSingular(%s) => %t\n", *p.word, pluralize.IsSingular(*p.word)) 71 | } 72 | 73 | if testCmd.Has(tflags.TestCmdPlural) { 74 | t.Logf("Plural(%s) => %s\n", *p.word, pluralize.Plural(*p.word)) 75 | } 76 | 77 | if testCmd.Has(tflags.TestCmdSingular) { 78 | t.Logf("Singular(%s) => %s\n", *p.word, pluralize.Singular(*p.word)) 79 | } 80 | } 81 | 82 | func TestIsPlural(t *testing.T) { 83 | tests := append(basicTests(), pluralTests()...) 84 | passed := 0 85 | failed := 0 86 | 87 | pluralize := NewClient() 88 | 89 | for i, testItem := range tests { 90 | if actual := pluralize.IsPlural(testItem.expected); actual == true { 91 | plogf(t, "PASS test[%d] func %s(%s) expected %t, actual %t", i, "IsPlural", 92 | testItem.input, true, actual) 93 | passed++ 94 | } else { 95 | t.Errorf("FAIL test[%d] func %s(%s) expected %t, actual %t", i, "IsPlural", 96 | testItem.input, true, actual) 97 | failed++ 98 | } 99 | } 100 | 101 | slog("TestIsPlural", passed, failed, len(tests)) 102 | } 103 | 104 | func TestIsSingular(t *testing.T) { 105 | tests := append(basicTests(), singularTests()...) 106 | passed := 0 107 | failed := 0 108 | 109 | pluralize := NewClient() 110 | 111 | for i, testItem := range tests { 112 | if actual := pluralize.IsSingular(testItem.input); actual == true { 113 | plogf(t, "PASS test[%d] func %s(%s) expected %t, actual %t", i, "IsSingular", 114 | testItem.input, true, actual) 115 | passed++ 116 | } else { 117 | t.Errorf("FAIL test[%d] func %s(%s) expected %t, actual %t", i, "IsSingular", 118 | testItem.input, true, actual) 119 | failed++ 120 | } 121 | } 122 | 123 | slog("TestIsSingular", passed, failed, len(tests)) 124 | } 125 | 126 | func TestPlural(t *testing.T) { 127 | tests := append(basicTests(), pluralTests()...) 128 | passed := 0 129 | failed := 0 130 | 131 | pluralize := NewClient() 132 | 133 | for i, testItem := range tests { 134 | if actual := pluralize.Plural(testItem.input); actual == testItem.expected { 135 | plogf(t, "PASS test[%d] func %s(%s) expected %s, actual %s", i, "Plural", 136 | testItem.input, testItem.expected, actual) 137 | passed++ 138 | } else { 139 | t.Errorf("FAIL test[%d] func %s(%s) expected %s, actual %s", i, "Plural", 140 | testItem.input, testItem.expected, actual) 141 | failed++ 142 | } 143 | } 144 | 145 | slog("TestPlural", passed, failed, len(tests)) 146 | } 147 | 148 | func TestSingular(t *testing.T) { 149 | tests := append(basicTests(), singularTests()...) 150 | passed := 0 151 | failed := 0 152 | 153 | pluralize := NewClient() 154 | 155 | for i, testItem := range tests { 156 | if actual := pluralize.Singular(testItem.expected); actual == testItem.input { 157 | plogf(t, "PASS test[%d] func %s(%s) expected %s, actual %s", i, "Singular", 158 | testItem.input, testItem.expected, actual) 159 | passed++ 160 | } else { 161 | t.Errorf("FAIL test[%d] func %s(%s) expected %s, actual %s", i, "Singular", 162 | testItem.input, testItem.expected, actual) 163 | failed++ 164 | } 165 | } 166 | 167 | slog("TestSingular", passed, failed, len(tests)) 168 | } 169 | 170 | func TestNewPluralRule(t *testing.T) { 171 | pluralize := NewClient() 172 | 173 | if pluralize.Plural(`regex`) != `regexes` { 174 | t.Fail() 175 | } 176 | 177 | pluralize.AddPluralRule(`(?i)gex$`, `gexii`) 178 | 179 | if pluralize.Plural(`regex`) != `regexii` { 180 | t.Fail() 181 | } 182 | } 183 | 184 | func TestNewSingularRule(t *testing.T) { 185 | pluralize := NewClient() 186 | 187 | if pluralize.Singular(`singles`) != `single` { 188 | t.Fail() 189 | } 190 | 191 | pluralize.AddSingularRule(`(?i)singles$`, `singular`) 192 | 193 | if pluralize.Singular(`singles`) != `singular` { 194 | t.Fail() 195 | } 196 | } 197 | 198 | func TestNewIrregularRule(t *testing.T) { 199 | pluralize := NewClient() 200 | 201 | if pluralize.Plural(`irregular`) != `irregulars` { 202 | t.Fail() 203 | } 204 | 205 | pluralize.AddIrregularRule(`irregular`, `regular`) 206 | 207 | if pluralize.Plural(`irregular`) != `regular` { 208 | t.Fail() 209 | } 210 | } 211 | 212 | func TestNewUncountableRule(t *testing.T) { 213 | pluralize := NewClient() 214 | 215 | if pluralize.Plural(`paper`) != `papers` { 216 | t.Fail() 217 | } 218 | 219 | pluralize.AddUncountableRule(`paper`) 220 | 221 | if pluralize.Plural(`paper`) != `paper` { 222 | t.Fail() 223 | } 224 | } 225 | 226 | func TestPluralize(t *testing.T) { 227 | const ( 228 | test = "test" 229 | tests = "tests" 230 | ) 231 | 232 | pluralize := NewClient() 233 | 234 | if pluralize.Pluralize(test, 0, false) != tests { 235 | t.Fail() 236 | } 237 | 238 | if pluralize.Pluralize(test, 0, false) != tests { 239 | t.Fail() 240 | } 241 | 242 | if pluralize.Pluralize(test, 1, false) != test { 243 | t.Fail() 244 | } 245 | 246 | if pluralize.Pluralize(test, 5, false) != tests { 247 | t.Fail() 248 | } 249 | 250 | if pluralize.Pluralize(test, 1, true) != `1 test` { 251 | t.Fail() 252 | } 253 | 254 | if pluralize.Pluralize(test, 5, true) != `5 tests` { 255 | t.Fail() 256 | } 257 | 258 | if pluralize.Pluralize(`蘋果`, 2, true) != `2 蘋果` { 259 | t.Fail() 260 | } 261 | } 262 | 263 | // Basic test cases of singular - plural pairs. 264 | func basicTests() []TestEntry { //nolint:funlen 265 | return []TestEntry{ 266 | // Uncountables. 267 | {`firmware`, `firmware`}, 268 | {`fish`, `fish`}, 269 | {`media`, `media`}, 270 | {`moose`, `moose`}, 271 | {`police`, `police`}, 272 | {`sheep`, `sheep`}, 273 | {`series`, `series`}, 274 | {`agenda`, `agenda`}, 275 | {`news`, `news`}, 276 | {`reindeer`, `reindeer`}, 277 | {`starfish`, `starfish`}, 278 | {`smallpox`, `smallpox`}, 279 | {`tennis`, `tennis`}, 280 | {`chickenpox`, `chickenpox`}, 281 | {`shambles`, `shambles`}, 282 | {`garbage`, `garbage`}, 283 | {`you`, `you`}, 284 | {`wildlife`, `wildlife`}, 285 | {`Staff`, `Staff`}, 286 | {`STAFF`, `STAFF`}, 287 | {`turquois`, `turquois`}, 288 | {`carnivorous`, `carnivorous`}, 289 | {`only`, `only`}, 290 | {`aircraft`, `aircraft`}, 291 | // Latin. 292 | {`veniam`, `veniam`}, 293 | // Pluralization. 294 | {`this`, `these`}, 295 | {`that`, `those`}, 296 | {`is`, `are`}, 297 | {`man`, `men`}, 298 | {`superman`, `supermen`}, 299 | {`ox`, `oxen`}, 300 | {`bus`, `buses`}, 301 | {`airbus`, `airbuses`}, 302 | {`railbus`, `railbuses`}, 303 | {`wife`, `wives`}, 304 | {`guest`, `guests`}, 305 | {`thing`, `things`}, 306 | {`mess`, `messes`}, 307 | {`guess`, `guesses`}, 308 | {`person`, `people`}, 309 | {`meteor`, `meteors`}, 310 | {`chateau`, `chateaus`}, 311 | {`lap`, `laps`}, 312 | {`cough`, `coughs`}, 313 | {`death`, `deaths`}, 314 | {`coach`, `coaches`}, 315 | {`boy`, `boys`}, 316 | {`toy`, `toys`}, 317 | {`guy`, `guys`}, 318 | {`girl`, `girls`}, 319 | {`chair`, `chairs`}, 320 | {`toe`, `toes`}, 321 | {`tiptoe`, `tiptoes`}, 322 | {`tomato`, `tomatoes`}, 323 | {`potato`, `potatoes`}, 324 | {`tornado`, `tornadoes`}, 325 | {`torpedo`, `torpedoes`}, 326 | {`hero`, `heroes`}, 327 | {`superhero`, `superheroes`}, 328 | {`volcano`, `volcanoes`}, 329 | {`canto`, `cantos`}, 330 | {`hetero`, `heteros`}, 331 | {`photo`, `photos`}, 332 | {`portico`, `porticos`}, 333 | {`quarto`, `quartos`}, 334 | {`kimono`, `kimonos`}, 335 | {`albino`, `albinos`}, 336 | {`cherry`, `cherries`}, 337 | {`piano`, `pianos`}, 338 | {`pro`, `pros`}, 339 | {`combo`, `combos`}, 340 | {`turbo`, `turbos`}, 341 | {`bar`, `bars`}, 342 | {`crowbar`, `crowbars`}, 343 | {`van`, `vans`}, 344 | {`tobacco`, `tobaccos`}, 345 | {`aficionado`, `aficionados`}, 346 | {`afficionado`, `afficionados`}, //nolint:misspell 347 | {`monkey`, `monkeys`}, 348 | {`neutrino`, `neutrinos`}, 349 | {`rhino`, `rhinos`}, 350 | {`steno`, `stenos`}, 351 | {`latino`, `latinos`}, 352 | {`casino`, `casinos`}, 353 | {`avocado`, `avocados`}, 354 | {`commando`, `commandos`}, 355 | {`tuxedo`, `tuxedos`}, 356 | {`speedo`, `speedos`}, 357 | {`dingo`, `dingoes`}, 358 | {`echo`, `echoes`}, 359 | {`nacho`, `nachos`}, 360 | {`motto`, `mottos`}, 361 | {`psycho`, `psychos`}, 362 | {`poncho`, `ponchos`}, 363 | {`pass`, `passes`}, 364 | {`ghetto`, `ghettos`}, 365 | {`mango`, `mangos`}, 366 | {`lady`, `ladies`}, 367 | {`bath`, `baths`}, 368 | {`professional`, `professionals`}, 369 | {`dwarf`, `dwarves`}, // Proper spelling is "dwarfs". 370 | {`encyclopedia`, `encyclopedias`}, 371 | {`louse`, `lice`}, 372 | {`roof`, `roofs`}, 373 | {`woman`, `women`}, 374 | {`formula`, `formulas`}, 375 | {`polyhedron`, `polyhedra`}, 376 | {`index`, `indices`}, // Maybe "indexes". 377 | {`matrix`, `matrices`}, 378 | {`vertex`, `vertices`}, 379 | {`axe`, `axes`}, // Could also be plural of "ax". 380 | {`pickaxe`, `pickaxes`}, 381 | {`crisis`, `crises`}, 382 | {`criterion`, `criteria`}, 383 | {`phenomenon`, `phenomena`}, 384 | {`addendum`, `addenda`}, 385 | {`datum`, `data`}, 386 | {`forum`, `forums`}, 387 | {`millennium`, `millennia`}, 388 | {`alumnus`, `alumni`}, 389 | {`medium`, `mediums`}, 390 | {`census`, `censuses`}, 391 | {`genus`, `genera`}, 392 | {`dogma`, `dogmata`}, 393 | {`life`, `lives`}, 394 | {`hive`, `hives`}, 395 | {`kiss`, `kisses`}, 396 | {`dish`, `dishes`}, 397 | {`human`, `humans`}, 398 | {`knife`, `knives`}, 399 | {`phase`, `phases`}, 400 | {`judge`, `judges`}, 401 | {`class`, `classes`}, 402 | {`witch`, `witches`}, 403 | {`church`, `churches`}, 404 | {`massage`, `massages`}, 405 | {`prospectus`, `prospectuses`}, 406 | {`syllabus`, `syllabi`}, 407 | {`viscus`, `viscera`}, 408 | {`cactus`, `cacti`}, 409 | {`hippopotamus`, `hippopotamuses`}, 410 | {`octopus`, `octopuses`}, 411 | {`platypus`, `platypuses`}, 412 | {`kangaroo`, `kangaroos`}, 413 | {`atlas`, `atlases`}, 414 | {`stigma`, `stigmata`}, 415 | {`schema`, `schemata`}, 416 | {`phenomenon`, `phenomena`}, 417 | {`diagnosis`, `diagnoses`}, 418 | {`mongoose`, `mongooses`}, 419 | {`mouse`, `mice`}, 420 | {`liturgist`, `liturgists`}, 421 | {`box`, `boxes`}, 422 | {`gas`, `gases`}, 423 | {`self`, `selves`}, 424 | {`chief`, `chiefs`}, 425 | {`quiz`, `quizzes`}, 426 | {`child`, `children`}, 427 | {`shelf`, `shelves`}, 428 | {`fizz`, `fizzes`}, 429 | {`tooth`, `teeth`}, 430 | {`thief`, `thieves`}, 431 | {`day`, `days`}, 432 | {`loaf`, `loaves`}, 433 | {`fix`, `fixes`}, 434 | {`spy`, `spies`}, 435 | {`vertebra`, `vertebrae`}, 436 | {`clock`, `clocks`}, 437 | {`lap`, `laps`}, 438 | {`cuff`, `cuffs`}, 439 | {`leaf`, `leaves`}, 440 | {`calf`, `calves`}, 441 | {`moth`, `moths`}, 442 | {`mouth`, `mouths`}, 443 | {`house`, `houses`}, 444 | {`proof`, `proofs`}, 445 | {`hoof`, `hooves`}, 446 | {`elf`, `elves`}, 447 | {`turf`, `turfs`}, 448 | {`craft`, `crafts`}, 449 | {`die`, `dice`}, 450 | {`penny`, `pennies`}, 451 | {`campus`, `campuses`}, 452 | {`virus`, `viri`}, 453 | {`iris`, `irises`}, 454 | {`bureau`, `bureaus`}, 455 | {`kiwi`, `kiwis`}, 456 | {`wiki`, `wikis`}, 457 | {`igloo`, `igloos`}, 458 | {`ninja`, `ninjas`}, 459 | {`pizza`, `pizzas`}, 460 | {`kayak`, `kayaks`}, 461 | {`canoe`, `canoes`}, 462 | {`tiding`, `tidings`}, 463 | {`pea`, `peas`}, 464 | {`drive`, `drives`}, 465 | {`nose`, `noses`}, 466 | {`movie`, `movies`}, 467 | {`status`, `statuses`}, 468 | {`alias`, `aliases`}, 469 | {`memorandum`, `memorandums`}, 470 | {`language`, `languages`}, 471 | {`plural`, `plurals`}, 472 | {`word`, `words`}, 473 | {`multiple`, `multiples`}, 474 | {`reward`, `rewards`}, 475 | {`sandwich`, `sandwiches`}, 476 | {`subway`, `subways`}, 477 | {`direction`, `directions`}, 478 | {`land`, `lands`}, 479 | {`row`, `rows`}, 480 | {`grow`, `grows`}, 481 | {`flow`, `flows`}, 482 | {`rose`, `roses`}, 483 | {`raise`, `raises`}, 484 | {`friend`, `friends`}, 485 | {`follower`, `followers`}, 486 | {`male`, `males`}, 487 | {`nail`, `nails`}, 488 | {`sex`, `sexes`}, 489 | {`tape`, `tapes`}, 490 | {`ruler`, `rulers`}, 491 | {`king`, `kings`}, 492 | {`queen`, `queens`}, 493 | {`zero`, `zeros`}, 494 | {`quest`, `quests`}, 495 | {`goose`, `geese`}, 496 | {`foot`, `feet`}, 497 | {`ex`, `exes`}, 498 | {`reflex`, `reflexes`}, 499 | {`heat`, `heats`}, 500 | {`train`, `trains`}, 501 | {`test`, `tests`}, 502 | {`pie`, `pies`}, 503 | {`fly`, `flies`}, 504 | {`eye`, `eyes`}, 505 | {`lie`, `lies`}, 506 | {`node`, `nodes`}, 507 | {`trade`, `trades`}, 508 | {`chinese`, `chinese`}, 509 | {`please`, `pleases`}, 510 | {`japanese`, `japanese`}, 511 | {`regex`, `regexes`}, 512 | {`license`, `licenses`}, 513 | {`zebra`, `zebras`}, 514 | {`general`, `generals`}, 515 | {`corps`, `corps`}, 516 | {`pliers`, `pliers`}, 517 | {`flyer`, `flyers`}, 518 | {`scissors`, `scissors`}, 519 | {`fireman`, `firemen`}, 520 | {`chirp`, `chirps`}, 521 | {`harp`, `harps`}, 522 | {`corpse`, `corpses`}, 523 | {`dye`, `dyes`}, 524 | {`move`, `moves`}, 525 | {`zombie`, `zombies`}, 526 | {`variety`, `varieties`}, 527 | {`talkie`, `talkies`}, 528 | {`walkie-talkie`, `walkie-talkies`}, 529 | {`groupie`, `groupies`}, 530 | {`goonie`, `goonies`}, 531 | {`lassie`, `lassies`}, 532 | {`foodie`, `foodies`}, 533 | {`faerie`, `faeries`}, 534 | {`collie`, `collies`}, 535 | {`obloquy`, `obloquies`}, 536 | {`looey`, `looies`}, 537 | {`osprey`, `ospreys`}, 538 | {`cover`, `covers`}, 539 | {`tie`, `ties`}, 540 | {`groove`, `grooves`}, 541 | {`bee`, `bees`}, 542 | {`ave`, `aves`}, 543 | {`wave`, `waves`}, 544 | {`wolf`, `wolves`}, 545 | {`airwave`, `airwaves`}, 546 | {`archive`, `archives`}, 547 | {`arch`, `arches`}, 548 | {`dive`, `dives`}, 549 | {`aftershave`, `aftershaves`}, 550 | {`cave`, `caves`}, 551 | {`grave`, `graves`}, 552 | {`gift`, `gifts`}, 553 | {`nerve`, `nerves`}, 554 | {`nerd`, `nerds`}, 555 | {`carve`, `carves`}, 556 | {`rave`, `raves`}, 557 | {`scarf`, `scarves`}, 558 | {`sale`, `sales`}, 559 | {`sail`, `sails`}, 560 | {`swerve`, `swerves`}, 561 | {`love`, `loves`}, 562 | {`dove`, `doves`}, 563 | {`glove`, `gloves`}, 564 | {`wharf`, `wharves`}, 565 | {`valve`, `valves`}, 566 | {`werewolf`, `werewolves`}, 567 | {`view`, `views`}, 568 | {`emu`, `emus`}, 569 | {`menu`, `menus`}, 570 | {`wax`, `waxes`}, 571 | {`fax`, `faxes`}, 572 | {`nut`, `nuts`}, 573 | {`crust`, `crusts`}, 574 | {`lemma`, `lemmata`}, 575 | {`anathema`, `anathemata`}, 576 | {`analysis`, `analyses`}, 577 | {`locus`, `loci`}, 578 | {`uterus`, `uteri`}, 579 | {`curriculum`, `curricula`}, 580 | {`quorum`, `quora`}, 581 | {`genie`, `genies`}, 582 | {`genius`, `geniuses`}, 583 | {`flower`, `flowers`}, 584 | {`crash`, `crashes`}, 585 | {`soul`, `souls`}, 586 | {`career`, `careers`}, 587 | {`planet`, `planets`}, 588 | {`son`, `sons`}, 589 | {`sun`, `suns`}, 590 | {`drink`, `drinks`}, 591 | {`diploma`, `diplomas`}, 592 | {`dilemma`, `dilemmas`}, 593 | {`grandma`, `grandmas`}, 594 | {`no`, `nos`}, 595 | {`yes`, `yeses`}, 596 | {`employ`, `employs`}, 597 | {`employee`, `employees`}, 598 | {`history`, `histories`}, 599 | {`story`, `stories`}, 600 | {`purchase`, `purchases`}, 601 | {`order`, `orders`}, 602 | {`key`, `keys`}, 603 | {`bomb`, `bombs`}, 604 | {`city`, `cities`}, 605 | {`sanity`, `sanities`}, 606 | {`ability`, `abilities`}, 607 | {`activity`, `activities`}, 608 | {`cutie`, `cuties`}, 609 | {`validation`, `validations`}, 610 | {`floaty`, `floaties`}, 611 | {`nicety`, `niceties`}, 612 | {`goalie`, `goalies`}, 613 | {`crawly`, `crawlies`}, 614 | {`duty`, `duties`}, 615 | {`scrutiny`, `scrutinies`}, 616 | {`deputy`, `deputies`}, 617 | {`beauty`, `beauties`}, 618 | {`bank`, `banks`}, 619 | {`family`, `families`}, 620 | {`tally`, `tallies`}, 621 | {`ally`, `allies`}, 622 | {`alley`, `alleys`}, 623 | {`valley`, `valleys`}, 624 | {`medley`, `medleys`}, 625 | {`melody`, `melodies`}, 626 | {`trolly`, `trollies`}, 627 | {`thunk`, `thunks`}, 628 | {`koala`, `koalas`}, 629 | {`special`, `specials`}, 630 | {`book`, `books`}, 631 | {`knob`, `knobs`}, 632 | {`crab`, `crabs`}, 633 | {`plough`, `ploughs`}, 634 | {`high`, `highs`}, 635 | {`low`, `lows`}, 636 | {`hiccup`, `hiccups`}, 637 | {`bonus`, `bonuses`}, 638 | {`circus`, `circuses`}, 639 | {`abacus`, `abacuses`}, 640 | {`phobia`, `phobias`}, 641 | {`case`, `cases`}, 642 | {`lace`, `laces`}, 643 | {`trace`, `traces`}, 644 | {`mage`, `mages`}, 645 | {`lotus`, `lotuses`}, 646 | {`motorbus`, `motorbuses`}, 647 | {`cutlas`, `cutlases`}, 648 | {`tequila`, `tequilas`}, 649 | {`liar`, `liars`}, 650 | {`delta`, `deltas`}, 651 | {`visa`, `visas`}, 652 | {`flea`, `fleas`}, 653 | {`favela`, `favelas`}, 654 | {`cobra`, `cobras`}, 655 | {`finish`, `finishes`}, 656 | {`gorilla`, `gorillas`}, 657 | {`mass`, `masses`}, 658 | {`face`, `faces`}, 659 | {`rabbit`, `rabbits`}, 660 | {`adventure`, `adventures`}, 661 | {`breeze`, `breezes`}, 662 | {`brew`, `brews`}, 663 | {`canopy`, `canopies`}, 664 | {`copy`, `copies`}, 665 | {`spy`, `spies`}, 666 | {`cave`, `caves`}, 667 | {`charge`, `charges`}, 668 | {`cinema`, `cinemas`}, 669 | {`coffee`, `coffees`}, 670 | {`favourite`, `favourites`}, 671 | {`themself`, `themselves`}, 672 | {`country`, `countries`}, 673 | {`issue`, `issues`}, 674 | {`authority`, `authorities`}, 675 | {`force`, `forces`}, 676 | {`objective`, `objectives`}, 677 | {`present`, `presents`}, 678 | {`industry`, `industries`}, 679 | {`believe`, `believes`}, 680 | {`century`, `centuries`}, 681 | {`category`, `categories`}, 682 | {`eve`, `eves`}, 683 | {`fee`, `fees`}, 684 | {`gene`, `genes`}, 685 | {`try`, `tries`}, 686 | {`currency`, `currencies`}, 687 | {`pose`, `poses`}, 688 | {`cheese`, `cheeses`}, 689 | {`clue`, `clues`}, 690 | {`cheer`, `cheers`}, 691 | {`litre`, `litres`}, 692 | {`money`, `monies`}, 693 | {`attorney`, `attorneys`}, 694 | {`balcony`, `balconies`}, 695 | {`cockney`, `cockneys`}, 696 | {`donkey`, `donkeys`}, 697 | {`honey`, `honeys`}, 698 | {`smiley`, `smilies`}, 699 | {`survey`, `surveys`}, 700 | {`whiskey`, `whiskeys`}, 701 | {`whisky`, `whiskies`}, 702 | {`volley`, `volleys`}, 703 | {`tongue`, `tongues`}, 704 | {`suit`, `suits`}, 705 | {`suite`, `suites`}, 706 | {`cruise`, `cruises`}, 707 | {`eave`, `eaves`}, 708 | {`consultancy`, `consultancies`}, 709 | {`pouch`, `pouches`}, 710 | {`wallaby`, `wallabies`}, 711 | {`abyss`, `abysses`}, 712 | {`weekly`, `weeklies`}, 713 | {`whistle`, `whistles`}, 714 | {`utilise`, `utilises`}, 715 | {`utilize`, `utilizes`}, 716 | {`mercy`, `mercies`}, 717 | {`mercenary`, `mercenaries`}, 718 | {`take`, `takes`}, 719 | {`flush`, `flushes`}, 720 | {`gate`, `gates`}, 721 | {`evolve`, `evolves`}, 722 | {`slave`, `slaves`}, 723 | {`native`, `natives`}, 724 | {`revolve`, `revolves`}, 725 | {`twelve`, `twelves`}, 726 | {`sleeve`, `sleeves`}, 727 | {`subjective`, `subjectives`}, 728 | {`stream`, `streams`}, 729 | {`beam`, `beams`}, 730 | {`foam`, `foams`}, 731 | {`callus`, `calluses`}, 732 | {`use`, `uses`}, 733 | {`beau`, `beaus`}, 734 | {`gateau`, `gateaus`}, 735 | {`fetus`, `fetuses`}, 736 | {`luau`, `luaus`}, 737 | {`pilau`, `pilaus`}, 738 | {`shoe`, `shoes`}, 739 | {`sandshoe`, `sandshoes`}, 740 | {`zeus`, `zeuses`}, 741 | {`nucleus`, `nuclei`}, 742 | {`sky`, `skies`}, 743 | {`beach`, `beaches`}, 744 | {`brush`, `brushes`}, 745 | {`hoax`, `hoaxes`}, 746 | {`scratch`, `scratches`}, 747 | {`nanny`, `nannies`}, 748 | {`negro`, `negroes`}, 749 | {`taco`, `tacos`}, 750 | {`cafe`, `cafes`}, 751 | {`cave`, `caves`}, 752 | {`giraffe`, `giraffes`}, 753 | {`goodwife`, `goodwives`}, 754 | {`housewife`, `housewives`}, 755 | {`safe`, `safes`}, 756 | {`save`, `saves`}, 757 | {`pocketknife`, `pocketknives`}, 758 | {`tartufe`, `tartufes`}, 759 | {`tartuffe`, `tartuffes`}, 760 | {`truffle`, `truffles`}, 761 | {`jefe`, `jefes`}, 762 | {`agrafe`, `agrafes`}, 763 | {`agraffe`, `agraffes`}, 764 | {`bouffe`, `bouffes`}, 765 | {`carafe`, `carafes`}, 766 | {`chafe`, `chafes`}, 767 | {`pouffe`, `pouffes`}, 768 | {`pouf`, `poufs`}, 769 | {`piaffe`, `piaffes`}, 770 | {`gaffe`, `gaffes`}, 771 | {`executive`, `executives`}, 772 | {`cove`, `coves`}, 773 | {`dove`, `doves`}, 774 | {`fave`, `faves`}, 775 | {`positive`, `positives`}, 776 | {`solve`, `solves`}, 777 | {`trove`, `troves`}, 778 | {`treasure`, `treasures`}, 779 | {`suave`, `suaves`}, 780 | {`bluff`, `bluffs`}, 781 | {`half`, `halves`}, 782 | {`knockoff`, `knockoffs`}, 783 | {`handkerchief`, `handkerchiefs`}, 784 | {`reed`, `reeds`}, 785 | {`reef`, `reefs`}, 786 | {`yourself`, `yourselves`}, 787 | {`sunroof`, `sunroofs`}, 788 | {`plateau`, `plateaus`}, 789 | {`radius`, `radii`}, 790 | {`stratum`, `strata`}, 791 | {`stratus`, `strati`}, 792 | {`focus`, `foci`}, 793 | {`fungus`, `fungi`}, 794 | {`appendix`, `appendices`}, 795 | {`seraph`, `seraphim`}, 796 | {`cherub`, `cherubim`}, 797 | {`memo`, `memos`}, 798 | {`cello`, `cellos`}, 799 | {`automaton`, `automata`}, 800 | {`button`, `buttons`}, 801 | {`crayon`, `crayons`}, 802 | {`captive`, `captives`}, 803 | {`abrasive`, `abrasives`}, 804 | {`archive`, `archives`}, 805 | {`additive`, `additives`}, 806 | {`hive`, `hives`}, 807 | {`beehive`, `beehives`}, 808 | {`olive`, `olives`}, 809 | {`black olive`, `black olives`}, 810 | {`chive`, `chives`}, 811 | {`adjective`, `adjectives`}, 812 | {`cattle drive`, `cattle drives`}, 813 | {`explosive`, `explosives`}, 814 | {`executive`, `executives`}, 815 | {`negative`, `negatives`}, 816 | {`fugitive`, `fugitives`}, 817 | {`progressive`, `progressives`}, 818 | {`laxative`, `laxatives`}, 819 | {`incentive`, `incentives`}, 820 | {`genesis`, `geneses`}, 821 | {`surprise`, `surprises`}, 822 | {`enterprise`, `enterprises`}, 823 | {`relative`, `relatives`}, 824 | {`positive`, `positives`}, 825 | {`perspective`, `perspectives`}, 826 | {`superlative`, `superlatives`}, 827 | {`afterlife`, `afterlives`}, 828 | {`native`, `natives`}, 829 | {`detective`, `detectives`}, 830 | {`collective`, `collectives`}, 831 | {`lowlife`, `lowlives`}, 832 | {`low-life`, `low-lives`}, 833 | {`strife`, `strifes`}, 834 | {`pony`, `ponies`}, 835 | {`phony`, `phonies`}, 836 | {`felony`, `felonies`}, 837 | {`colony`, `colonies`}, 838 | {`symphony`, `symphonies`}, 839 | {`semicolony`, `semicolonies`}, 840 | {`radiotelephony`, `radiotelephonies`}, 841 | {`company`, `companies`}, 842 | {`ceremony`, `ceremonies`}, 843 | {`carnivore`, `carnivores`}, 844 | {`emphasis`, `emphases`}, 845 | {`abuse`, `abuses`}, 846 | {`ass`, `asses`}, 847 | {`mile`, `miles`}, 848 | {`consensus`, `consensuses`}, 849 | {`coatdress`, `coatdresses`}, 850 | {`courthouse`, `courthouses`}, 851 | {`playhouse`, `playhouses`}, 852 | {`crispness`, `crispnesses`}, 853 | {`racehorse`, `racehorses`}, 854 | {`greatness`, `greatnesses`}, 855 | {`demon`, `demons`}, 856 | {`lemon`, `lemons`}, 857 | {`pokemon`, `pokemon`}, 858 | {`pokémon`, `pokémon`}, 859 | {`christmas`, `christmases`}, 860 | {`zymase`, `zymases`}, 861 | {`accomplice`, `accomplices`}, 862 | {`amice`, `amices`}, 863 | {`titmouse`, `titmice`}, 864 | {`slice`, `slices`}, 865 | {`base`, `bases`}, 866 | {`database`, `databases`}, 867 | {`rise`, `rises`}, 868 | {`uprise`, `uprises`}, 869 | {`size`, `sizes`}, 870 | {`prize`, `prizes`}, 871 | {`booby`, `boobies`}, 872 | {`hobby`, `hobbies`}, 873 | {`baby`, `babies`}, 874 | {`cookie`, `cookies`}, 875 | {`budgie`, `budgies`}, 876 | {`calorie`, `calories`}, 877 | {`brownie`, `brownies`}, 878 | {`lolly`, `lollies`}, 879 | {`hippie`, `hippies`}, 880 | {`smoothie`, `smoothies`}, 881 | {`techie`, `techies`}, 882 | {`specie`, `species`}, 883 | {`quickie`, `quickies`}, 884 | {`pixie`, `pixies`}, 885 | {`rotisserie`, `rotisseries`}, 886 | {`porkpie`, `porkpies`}, 887 | {`newbie`, `newbies`}, 888 | {`veggie`, `veggies`}, 889 | {`bourgeoisie`, `bourgeoisies`}, 890 | {`party`, `parties`}, 891 | {`apology`, `apologies`}, 892 | {`ancestry`, `ancestries`}, 893 | {`anomaly`, `anomalies`}, 894 | {`anniversary`, `anniversaries`}, 895 | {`battery`, `batteries`}, 896 | {`nappy`, `nappies`}, 897 | {`hanky`, `hankies`}, 898 | {`junkie`, `junkies`}, 899 | {`hogtie`, `hogties`}, 900 | {`footsie`, `footsies`}, 901 | {`curry`, `curries`}, 902 | {`fantasy`, `fantasies`}, 903 | {`housefly`, `houseflies`}, 904 | {`falsy`, `falsies`}, 905 | {`doggy`, `doggies`}, 906 | {`carny`, `carnies`}, 907 | {`cabby`, `cabbies`}, 908 | {`charlie`, `charlies`}, 909 | {`bookie`, `bookies`}, 910 | {`auntie`, `aunties`}, 911 | // Prototype inheritance. 912 | {`constructor`, `constructors`}, 913 | // Non-standard case. 914 | {`randomWord`, `randomWords`}, 915 | {`camelCase`, `camelCases`}, 916 | {`PascalCase`, `PascalCases`}, 917 | {`Alumnus`, `Alumni`}, 918 | {`CHICKEN`, `CHICKENS`}, 919 | {`日本語`, `日本語`}, 920 | {`한국`, `한국`}, 921 | {`中文`, `中文`}, 922 | {`اللغة العربية`, `اللغة العربية`}, 923 | {`四 chicken`, `四 chickens`}, 924 | {`Order2`, `Order2s`}, 925 | {`Work Order2`, `Work Order2s`}, 926 | {`SoundFX2`, `SoundFX2s`}, 927 | {`oDonald`, `oDonalds`}, 928 | } 929 | } 930 | 931 | // Odd plural to singular tests. 932 | func singularTests() []TestEntry { 933 | return []TestEntry{ 934 | {`dingo`, `dingos`}, 935 | {`mango`, `mangoes`}, 936 | {`echo`, `echos`}, 937 | {`ghetto`, `ghettoes`}, 938 | {`nucleus`, `nucleuses`}, 939 | {`bureau`, `bureaux`}, 940 | {`seraph`, `seraphs`}, 941 | } 942 | } 943 | 944 | // Odd singular to plural tests. 945 | func pluralTests() []TestEntry { 946 | return []TestEntry{ 947 | {`plateaux`, `plateaux`}, 948 | {`axis`, `axes`}, 949 | {`basis`, `bases`}, 950 | {`automatum`, `automata`}, 951 | {`thou`, `you`}, 952 | {`axiS`, `axes`}, 953 | {`passerby`, `passersby`}, 954 | } 955 | } 956 | --------------------------------------------------------------------------------