From 6c3a5243f05c31fb4efb203678533d3b657fd812 Mon Sep 17 00:00:00 2001 From: Chance Date: Mon, 29 Aug 2022 01:22:31 -0400 Subject: [PATCH] v0.1.0 --- README.md | 55 +++++++++++++++++++++++++++++ caps.go | 57 ++++++++++++++++-------------- examples_test.go | 92 ++++++++++++++++++++++++++++++++++-------------- go.mod | 2 +- text/text.go | 48 ++++++++++++------------- 5 files changed, 176 insertions(+), 78 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..48dbc2d --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# caps + +caps is a case conversion library for Go. It was built with the following +priorites: configurability, consistency, correctness, ergonomic, and performant +in mind, in that order. + +Out of the box, the following case conversion are supported: + +- Camel Case (e.g. CamelCase) +- Lower Camel Case (e.g. lowerCamelCase) +- Snake Case (e.g. snake_case) +- Screaming Snake Case (e.g. SCREAMING_SNAKE_CASE) +- Kebab Case (e.g. kebab-case) +- Screaming Kebab Case(e.g. SCREAMING-KEBAB-CASE) +- Dot Notation Case (e.g. dot.notation.case) +- Title Case (e.g. Title Case) +- Other deliminations + +Word boundaries are determined by the `caps.Formatter`. The provided implementation, `caps.FormatterImpl`, +delegates the boundary detection to `caps.Tokenizer`. The provided implementation, `caps.TokenizerImpl`, +uses the following tokens as delimiters: `" _.!?:;$-(){}[]#@&+~"`. + +`caps.StdFormatter` also allows users to register `caps.Replacement`s for acronym replacement. The default list is: + +```go +{"Http", "HTTP"} +{"Https", "HTTPS"} +{"Id", "ID"} +{"Ip", "IP"} +{"Html", "HTML"} +{"Xml", "XML"} +{"Json", "JSON"} +{"Csv", "CSV"} +{"Aws", "AWS"} +{"Gcp", "GCP"} +{"Sql", "SQL"} +``` + +If you would like to add or remove entries from that list, you have a few +options. + +You can pass a new instance of `caps.StdFormatter` with a new set of +`caps.Replacement` (likely preferred). + +You can create your own `caps.Formatter`. This could be as simple as +implementing the single `Format` method, calling `caps.DefaultFormatter.Format`, +and then modifying the result. + +Finally, if you are so inclined, you can update `caps.DefaultFormatter`. Just be aware that the +module was not built with thread-safety in mind so you should set it once. +Otherwise, you'll need guard your usage of the library accordingly. + +## License + +MIT diff --git a/caps.go b/caps.go index e95c6ce..a4bfdda 100644 --- a/caps.go +++ b/caps.go @@ -90,7 +90,7 @@ type Tokenizer interface { // // style: Expected output caps.Style of the string. // repStyle: The caps.ReplaceStyle to use if a word needs to be replaced. -// join: The delimiter to use when joining the words. For CamelCase, this is an empty string. +// join: The delimiter to use when joining the words. For Camel, this is an empty string. // allowedSymbols: The set of allowed symbols. If set, these should take precedence over any delimiters // numberRules: Any custom rules dictating how to handle special characters in numbers. type Formatter interface { @@ -153,8 +153,6 @@ func (t TokenizerImpl) Tokenize(str string, allowedSymbols []rune, numberRules m allowed := newRunes(allowedSymbols) for i, r := range str { - rStr := string(r) - _ = rStr switch { case unicode.IsUpper(r): if foundLower && current.Len() > 0 { @@ -304,10 +302,10 @@ type ReplaceStyle uint8 type ( Replacement struct { - // Camelcase variant of the word which should be replaced. + // Camel case variant of the word which should be replaced. // e.g. "Http" Camel string - // Screaming (all uppercase) representation of the word to replace. + // Screaming (all upper case) representation of the word to replace. // e.g. "HTTP" Screaming string } @@ -318,8 +316,8 @@ type Style uint8 const ( StyleLower Style = iota // The output should be lowercase (e.g. "an_example") StyleScreaming // The output should be screaming (e.g. "AN_EXAMPLE") - StyleCamel // The output should be camelcase (e.g. "AnExample") - StyleLowerCamel // The output should be lower camelcase (e.g. "anExample") + StyleCamel // The output should be camel case (e.g. "AnExample") + StyleLowerCamel // The output should be lower camel case (e.g. "anExample") ) const ( @@ -616,7 +614,7 @@ func loadOpts(opts []Opts) Opts { result := Opts{ AllowedSymbols: "", Formatter: DefaultFormatter, - ReplaceStyle: ReplaceStyleNotSpecified, + ReplaceStyle: ReplaceStyleScreaming, } if len(opts) == 0 { return result @@ -628,6 +626,13 @@ func loadOpts(opts []Opts) Opts { if opts[0].Formatter != nil { result.Formatter = opts[0].Formatter } + if opts[0].ReplaceStyle != ReplaceStyleNotSpecified { + result.ReplaceStyle = opts[0].ReplaceStyle + } + if opts[0].NumberRules != nil { + result.NumberRules = opts[0].NumberRules + } + return result } @@ -653,7 +658,7 @@ func WithoutNumbers[T ~string](s T) T { }, string(s))) } -// ToCamel transforms the case of str into Camelcase (e.g. AnExampleString) using +// ToCamel transforms the case of str into Camel Case (e.g. AnExampleString) using // either the provided Formatter or the DefaultFormatter otherwise. // // The default Formatter detects case so that "AN_EXAMPLE_STRING" becomes "AnExampleString". @@ -661,14 +666,14 @@ func WithoutNumbers[T ~string](s T) T { // so long as opts.ReplacementStyle is set to ReplaceStyleScreaming. A ReplaceStyle of // ReplaceStyleCamel would result in "SomeJson". // -// caps.ToCamel("This is [an] {example}${id32}.") // thisIsAnExampleID32 -// caps.ToCamel("AN_EXAMPLE_STRING", ) // anExampleString +// caps.ToCamel("This is [an] {example}${id32}.") // ThisIsAnExampleID32 +// caps.ToCamel("AN_EXAMPLE_STRING", ) // AnExampleString func ToCamel[T ~string](str T, options ...Opts) T { opts := loadOpts(options) return T(opts.Formatter.Format(StyleCamel, opts.ReplaceStyle, string(str), "", []rune(opts.AllowedSymbols), opts.NumberRules)) } -// ToLowerCamel transforms the case of str into Lower Camelcase (e.g. anExampleString) using +// ToLowerCamel transforms the case of str into Lower Camel Case (e.g. anExampleString) using // either the provided Formatter or the DefaultFormatter otherwise. // // The default Formatter detects case so that "AN_EXAMPLE_STRING" becomes "anExampleString". @@ -682,7 +687,7 @@ func ToLowerCamel[T ~string](str T, options ...Opts) T { return T(opts.Formatter.Format(StyleLowerCamel, opts.ReplaceStyle, string(str), "", []rune(opts.AllowedSymbols), opts.NumberRules)) } -// ToSnake transforms the case of str into Lower Snakecase (e.g. an_example_string) using +// ToSnake transforms the case of str into Lower Snake Case (e.g. an_example_string) using // either the provided Formatter or the DefaultFormatter otherwise. // // caps.ToSnake("This is [an] {example}${id32}.") // this_is_an_example_id_32 @@ -690,7 +695,7 @@ func ToSnake[T ~string](str T, options ...Opts) T { return ToDelimited(str, '_', true, options...) } -// ToScreamingSnake transforms the case of str into Screaming Snakecase (e.g. +// ToScreamingSnake transforms the case of str into Screaming Snake Case (e.g. // AN_EXAMPLE_STRING) using either the provided Formatter or the // DefaultFormatter otherwise. // @@ -699,41 +704,41 @@ func ToScreamingSnake[T ~string](str T, options ...Opts) T { return ToDelimited(str, '_', false, options...) } -// ToKebab transforms the case of str into Lower Kebabcase (e.g. an-example-string) using +// ToKebab transforms the case of str into Lower Kebab Case (e.g. an-example-string) using // either the provided Formatter or the DefaultFormatter otherwise. // // caps.ToKebab("This is [an] {example}${id32}.") // this-is-an-example-id-32 func ToKebab[T ~string](str T, options ...Opts) T { - return ToDelimited(str, '_', true, options...) + return ToDelimited(str, '-', true, options...) } -// ToScreamingKebab transforms the case of str into Screaming Kebab (e.g. +// ToScreamingKebab transforms the case of str into Screaming Kebab Snake (e.g. // AN-EXAMPLE-STRING) using either the provided Formatter or the // DefaultFormatter otherwise. // -// caps.ToScreamingSnake("This is [an] {example}${id32}.") // THIS-IS-AN-EXAMPLE-ID-32 +// caps.ToScreamingKebab("This is [an] {example}${id32}.") // THIS-IS-AN-EXAMPLE-ID-32 func ToScreamingKebab[T ~string](str T, options ...Opts) T { - return ToDelimited(str, '_', false, options...) + return ToDelimited(str, '-', false, options...) } -// ToDot transforms the case of str into Lower Dot notation case (e.g. an.example.string) using +// ToDot transforms the case of str into Lower Dot Notation Case (e.g. an.example.string) using // either the provided Formatter or the DefaultFormatter otherwise. // // caps.ToDot("This is [an] {example}${id32}.") // this.is.an.example.id.32 func ToDot[T ~string](str T, options ...Opts) T { - return ToDelimited(str, '_', true, options...) + return ToDelimited(str, '.', true, options...) } -// ToScreamingKebab transforms the case of str into Screaming Kebab (e.g. +// ToScreamingKebab transforms the case of str into Screaming Kebab Case (e.g. // AN-EXAMPLE-STRING) using either the provided Formatter or the // DefaultFormatter otherwise. // // caps.ToScreamingDot("This is [an] {example}${id32}.") // THIS.IS.AN.EXAMPLE.ID.32 func ToScreamingDot[T ~string](str T, options ...Opts) T { - return ToDelimited(str, '_', false, options...) + return ToDelimited(str, '.', false, options...) } -// ToTitle transforms the case of str into Lower Dot notation case (e.g. An Example String) using +// ToTitle transforms the case of str into Title Case (e.g. An Example String) using // either the provided Formatter or the DefaultFormatter otherwise. // // caps.ToTitle("This is [an] {example}${id32}.") // This Is An Example ID 32 @@ -752,8 +757,8 @@ func ToTitle[T ~string](str T, options ...Opts) T { // # Example // // caps.ToDelimited("This is [an] {example}${id}.#32", '.', true) // this.is.an.example.id.32 -// caps.ToDelimited("This is [an] {example}${id32}.break32", '.', false) // THIS.IS.AN.EXAMPLE.ID.BREAK.32 -// caps.ToDelimited("This is [an] {example}${id32}.v32", '.', true, caps.Opts{AllowedSymbols: "$" }) // this.is.an.example.id.$.v32 +// caps.ToDelimited("This is [an] {example}${id}.break32", '.', false) // THIS.IS.AN.EXAMPLE.ID.BREAK.32 +// caps.ToDelimited("This is [an] {example}${id}.v32", '.', true, caps.Opts{AllowedSymbols: "$"}) // this.is.an.example.$.id.v32 func ToDelimited[T ~string](str T, delimiter rune, lowercase bool, options ...Opts) T { opts := loadOpts(options) var style Style diff --git a/examples_test.go b/examples_test.go index 1816df3..b431796 100644 --- a/examples_test.go +++ b/examples_test.go @@ -1,38 +1,76 @@ package caps_test -func ExampleToCamel() { - // fmt.Println(caps.ToCamel("This is [an] {example}${id32}.")) - // fmt.Println(caps.ToCamel("This is [an] {example}${id32}.break32")) - // fmt.Println(caps.ToCamel("This example allows for $ symbols", caps.Opts{AllowedSymbols: "$"})) +import ( + "fmt" - // customReplacer := caps.NewFormatter([]caps.R{{"Http", "HTTP"}, {"Https", "HTTPS"}}) - // fmt.Println(caps.ToCamel("No Id just http And Https", caps.Opts{Formatter: customReplacer})) + "github.com/chanced/caps" +) - // Outputx: +func ExampleToCamel() { + fmt.Println(caps.ToCamel("This is [an] {example}${id32}.")) + fmt.Println(caps.ToCamel("AN_EXAMPLE_STRING")) + // Output: + // ThisIsAnExampleID32 + // AnExampleString +} + +func ExampleToLowerCamel() { + fmt.Println(caps.ToLowerCamel("This is [an] {example}${id32}.")) + // Output: // thisIsAnExampleID32 - // thisIsAnExampleID32Break32 - // thisExampleAllowsFor$symbols - // noIdJustHTTPAndHTTPS +} + +func ExampleToSnake() { + fmt.Println(caps.ToSnake("This is [an] {example}${id32}.")) + fmt.Println(caps.ToSnake("v3.2.2")) + // Output: + // this_is_an_example_id_32 + // v3_2_2 +} + +func ExampleToScreamingSnake() { + fmt.Println(caps.ToScreamingSnake("This is [an] {example}${id32}.")) + // Output: + // THIS_IS_AN_EXAMPLE_ID_32 +} + +func ExampleToKebab() { + fmt.Println(caps.ToKebab("This is [an] {example}${id32}.")) + // Output: + // this-is-an-example-id-32 +} + +func ExampleToScreamingKebab() { + fmt.Println(caps.ToScreamingKebab("This is [an] {example}${id32}.")) + // Output: + // THIS-IS-AN-EXAMPLE-ID-32 +} + +func ExampleToDot() { + fmt.Println(caps.ToDot("This is [an] {example}${id32}.")) + // Output: + // this.is.an.example.id.32 +} + +func ExampleToScreamingDot() { + fmt.Println(caps.ToScreamingDot("This is [an] {example}${id32}.")) + // Output: + // THIS.IS.AN.EXAMPLE.ID.32 } func ExampleToDelimited() { - // fmt.Println(caps.ToDelimited("A # B _ C", '.', true)) - // fmt.Println(caps.ToDelimited("$id", '.', false)) - // fmt.Println(caps.ToDelimited("$id", '.', true, caps.Opts{AllowedSymbols: "$"})) - // fmt.Println(caps.ToDelimited("fromCamelcaseString", '.', true)) - // Outputx: - // a.b.c - // ID - // $id - // from.camelcase.string + fmt.Println(caps.ToDelimited("This is [an] {example}${id}.#32", '.', true)) + fmt.Println(caps.ToDelimited("This is [an] {example}${id}.break32", '.', false)) + fmt.Println(caps.ToDelimited("This is [an] {example}${id}.v32", '.', true, caps.Opts{AllowedSymbols: "$"})) + + // Output: + // this.is.an.example.id.32 + // THIS.IS.AN.EXAMPLE.ID.BREAK.32 + // this.is.an.example.$.id.v32 } -func ExampleToSnake() { - // fmt.Println(caps.ToSnake("A long string with spaces")) - // fmt.Println(caps.ToSnake(strings.ToLower("A_SCREAMING_SNAKE_MUST_BE_LOWERED_FIRST"))) - // fmt.Println(caps.ToSnake("$word", caps.Opts{AllowedSymbols: "$"})) - // OutputX: - // a_long_string_with_spaces - // a_screaming_snake_must_be_lowered_first - // $word +func ExampleToTitle() { + fmt.Println(caps.ToTitle("This is [an] {example}${id32}.")) + // Output: + // This Is An Example ID 32 } diff --git a/go.mod b/go.mod index 8540a3e..00c030d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/chanced/caps -go 1.19 +go 1.18 diff --git a/text/text.go b/text/text.go index f9099e5..f3c2f40 100644 --- a/text/text.go +++ b/text/text.go @@ -1,34 +1,34 @@ package text -import "strings" +// import "strings" -type Text string +// type Text string -func (t Text) String() string { - return string(t) -} +// func (t Text) String() string { +// return string(t) +// } -func (t Text) ToLower() Text { - return Text(strings.ToLower(t.String())) -} +// func (t Text) ToLower() Text { +// return Text(strings.ToLower(t.String())) +// } -func (t Text) ToUpper() Text { - return Text(strings.ToUpper(t.String())) -} +// func (t Text) ToUpper() Text { +// return Text(strings.ToUpper(t.String())) +// } -func (t Text) ToSnake() Text { - panic("not implemented") -} +// func (t Text) ToSnake() Text { +// panic("not implemented") +// } -func (t Text) ToScreamingSnake() Text { - // return Text(strcase.ToScreamingSnake(t.String())) - panic("not implemented") -} +// func (t Text) ToScreamingSnake() Text { +// // return Text(strcase.ToScreamingSnake(t.String())) +// panic("not implemented") +// } -func (t Text) ReplaceAll(old string, new string) Text { - return Text(strings.Replace(t.String(), old, new, -1)) -} +// func (t Text) ReplaceAll(old string, new string) Text { +// return Text(strings.Replace(t.String(), old, new, -1)) +// } -func (t Text) Replace(old, new string, n int) Text { - return Text(strings.Replace(t.String(), old, new, n)) -} +// func (t Text) Replace(old, new string, n int) Text { +// return Text(strings.Replace(t.String(), old, new, n)) +// }