From d21113547a728b979d151199203af17f6e3ae1c2 Mon Sep 17 00:00:00 2001 From: Christopher Murphy Date: Fri, 7 Oct 2022 16:43:47 -0600 Subject: [PATCH] feat: add support for configuring phrase casing Add supporting tests. Refactor a few generator references. --- cmd/fname/fname.go | 10 ++++++ generator.go | 89 ++++++++++++++++++++++++++++++++++++---------- generator_test.go | 59 +++++++++++++++++++++++++++++- go.mod | 2 ++ go.sum | 2 ++ 5 files changed, 142 insertions(+), 20 deletions(-) diff --git a/cmd/fname/fname.go b/cmd/fname/fname.go index 9997c76..e050911 100644 --- a/cmd/fname/fname.go +++ b/cmd/fname/fname.go @@ -42,6 +42,7 @@ func main() { pflag.Usage = generateUsage var ( + casing string = "lower" delimiter string help bool ver bool @@ -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)") @@ -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)) } diff --git a/generator.go b/generator.go index 1c4fe8a..98e453b 100644 --- a/generator.go +++ b/generator.go @@ -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 @@ -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 } diff --git a/generator_test.go b/generator_test.go index 63a6cd8..ecc9be2 100644 --- a/generator_test.go +++ b/generator_test.go @@ -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.") } @@ -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("_")) @@ -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") + } + } +} diff --git a/go.mod b/go.mod index f31c535..a20e648 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 287f6fa..ba20ec9 100644 --- a/go.sum +++ b/go.sum @@ -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=