Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cmd/fname/fname.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func main() {
pflag.Usage = generateUsage

var (
casing string = "lower"
delimiter string
help bool
ver bool
Expand All @@ -51,6 +52,7 @@ func main() {
// TODO: add option to use custom dictionary
)

pflag.StringVarP(&casing, "casing", "c", casing, "case of generated names: lower, upper, or title")
pflag.StringVarP(&delimiter, "delimiter", "d", delimiter, "delimiter to use between words")
pflag.IntVarP(&quantity, "quantity", "q", quantity, "number of name phrases to generate")
pflag.UintVarP(&size, "size", "z", size, "number of words per phrase (minimum 2, maximum 4)")
Expand All @@ -70,6 +72,14 @@ func main() {
}

opts := []fname.GeneratorOption{}

c, err := fname.ParseCasing(casing)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %s", err)
os.Exit(1)
}
opts = append(opts, fname.WithCasing(c))

if delimiter != "" {
opts = append(opts, fname.WithDelimiter(delimiter))
}
Expand Down
89 changes: 70 additions & 19 deletions generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,22 @@ import (
"math/rand"
"strings"
"time"

"golang.org/x/text/cases"
"golang.org/x/text/language"
)

type Casing string

const (
Lower Casing = "lower"
Upper Casing = "upper"
Title Casing = "title"
)

// Generator is a random name generator.
type Generator struct {
casing Casing
dict *Dictionary
delimiter string
rand *rand.Rand
Expand All @@ -19,61 +31,100 @@ type Generator struct {
// GeneratorOption is a function that configures a Generator.
type GeneratorOption func(*Generator)

// WithCasing sets the casing used to format the generated name.
func WithCasing(casing Casing) GeneratorOption {
return func(g *Generator) {
g.casing = casing
}
}

// WithDelimiter sets the delimiter used to join words.
func WithDelimiter(delimiter string) GeneratorOption {
return func(r *Generator) {
r.delimiter = delimiter
return func(g *Generator) {
g.delimiter = delimiter
}
}

// WithSeed sets the seed used to generate random numbers.
func WithSeed(seed int64) GeneratorOption {
return func(r *Generator) {
r.rand.Seed(seed)
return func(g *Generator) {
g.rand.Seed(seed)
}
}

// WithSize sets the number of words in the generated name.
func WithSize(size uint) GeneratorOption {
return func(r *Generator) {
r.size = size
return func(g *Generator) {
g.size = size
}
}

// NewGenerator creates a new Generator.
func NewGenerator(opts ...GeneratorOption) *Generator {
r := &Generator{
g := &Generator{
casing: Lower,
dict: NewDictionary(),
delimiter: "-",
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
size: 2,
}
for _, opt := range opts {
opt(r)
opt(g)
}
return r
return g
}

// Generate generates a random name.
func (r *Generator) Generate() (string, error) {
func (g *Generator) Generate() (string, error) {
// TODO: address case where adjective and noun are the same, such as "orange-orange" or "sound-sound"
adjective := r.dict.adectives[r.rand.Intn(r.dict.LengthAdjective())]
noun := r.dict.nouns[r.rand.Intn(r.dict.LengthNoun())]
adjective := g.dict.adectives[g.rand.Intn(g.dict.LengthAdjective())]
noun := g.dict.nouns[g.rand.Intn(g.dict.LengthNoun())]
words := []string{adjective, noun}

switch r.size {
switch g.size {
case 2:
return strings.Join(words, r.delimiter), nil
// do nothing
case 3:
verb := r.dict.verbs[r.rand.Intn(r.dict.LengthVerb())]
verb := g.dict.verbs[g.rand.Intn(g.dict.LengthVerb())]
words = append(words, verb)
case 4:
verb := r.dict.verbs[r.rand.Intn(r.dict.LengthVerb())]
verb := g.dict.verbs[g.rand.Intn(g.dict.LengthVerb())]
words = append(words, verb)
adverb := r.dict.adverbs[r.rand.Intn(r.dict.LengthAdverb())]
adverb := g.dict.adverbs[g.rand.Intn(g.dict.LengthAdverb())]
words = append(words, adverb)
default:
return "", fmt.Errorf("invalid size: %d", r.size)
return "", fmt.Errorf("invalid size: %d", g.size)
}
return strings.Join(g.applyCasing(words...), g.delimiter), nil
}

// ParseCasing parses a string into a casing.
func ParseCasing(casing string) (Casing, error) {
switch casing {
case "lower":
return Lower, nil
case "upper":
return Upper, nil
case "title":
return Title, nil
default:
return "", fmt.Errorf("invalid casing: %s", casing)
}
}

var titleCaser = cases.Title(language.English)

var casingMap = map[Casing]func(string) string{
Lower: strings.ToLower,
Upper: strings.ToUpper,
Title: titleCaser.String,
}

func (g *Generator) applyCasing(words ...string) []string {
if fn, ok := casingMap[g.casing]; ok {
for i, word := range words {
words[i] = fn(word)
}
}
return strings.Join(words, r.delimiter), nil
return words
}
59 changes: 58 additions & 1 deletion generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@ func TestNewGenerator(t *testing.T) {

t.Log("\tWhen creating a new Generator with custom values")
{
g := NewGenerator(WithDelimiter("_"), WithSize(3), WithSeed(12345))
g := NewGenerator(WithCasing(Title), WithDelimiter("_"), WithSize(3), WithSeed(12345))
if g == nil {
t.Fatal("\t\tShould be able to create a Generator instance.")
}
t.Log("\t\tShould be able to create a Generator instance.")

if g.casing != Title {
t.Error("\t\tShould be able to set the casing.")
}

if g.size != 3 {
t.Fatal("\t\tShould be able to set the size of the phrase.")
}
Expand Down Expand Up @@ -67,6 +71,21 @@ func TestGenerate(t *testing.T) {
t.Log("\t\tShould be able to generate a phrase with 2 parts.")
}

t.Log("\tWhen generating a phrase with a custom case")
{
g := NewGenerator(WithCasing(Title))
phrase, err := g.Generate()
if err != nil {
t.Fatal("\t\tShould be able to generate a phrase without error.")
}
t.Log("\t\tShould be able to generate a phrase without error.")

c := phrase[0]
if c < 'A' || c > 'Z' {
t.Fatal("\t\tShould be able to generate a phrase with a title case.")
}
}

t.Log("\tWhen generating a phrase with a custom delimiter")
{
g := NewGenerator(WithDelimiter("_"))
Expand Down Expand Up @@ -165,3 +184,41 @@ func TestGenerate(t *testing.T) {
}
}
}

func TestParseCasing(t *testing.T) {
t.Log("Given the need to parse casing strings")
{
t.Log("\tWhen parsing a valid casing string")
{
testCases := []struct {
name string
c Casing
}{
{"lower", Lower},
{"upper", Upper},
{"title", Title},
}
for _, tc := range testCases {
c, err := ParseCasing(tc.name)
if err != nil {
t.Fatalf("\t\tShould be able to parse a valid casing string : %v", err)
}
t.Log("\t\tShould be able to parse a valid casing string")

if c != tc.c {
t.Fatalf("\t\tShould be able to parse a valid casing string : got %v, want %v", c, tc.c)
}
t.Log("\t\tShould be able to parse a valid casing string")
}
}

t.Log("\tWhen parsing an invalid casing string")
{
_, err := ParseCasing("invalid")
if err == nil {
t.Fatal("\t\tShould not be able to parse an invalid casing string")
}
t.Log("\t\tShould not be able to parse an invalid casing string")
}
}
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ module github.com/splode/fname
go 1.19

require github.com/spf13/pflag v1.0.5

require golang.org/x/text v0.3.7
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=