mirror of https://github.com/dmarkham/enumer.git
Merge pull request #6 from THE108/transform
Enum value string representation transformation feature
This commit is contained in:
commit
5b38bf6a65
|
@ -0,0 +1,28 @@
|
||||||
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### Go template
|
||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
|
|
||||||
|
.idea
|
30
README.md
30
README.md
|
@ -1,8 +1,8 @@
|
||||||
#Enumer
|
# Enumer
|
||||||
Enumer is a tool to generate Go code that adds useful methods to Go enums (constants with a specific type).
|
Enumer is a tool to generate Go code that adds useful methods to Go enums (constants with a specific type).
|
||||||
It started as a fork of [Rob Pike’s Stringer tool](https://godoc.org/golang.org/x/tools/cmd/stringer).
|
It started as a fork of [Rob Pike’s Stringer tool](https://godoc.org/golang.org/x/tools/cmd/stringer).
|
||||||
|
|
||||||
##Generated functions and methods
|
## Generated functions and methods
|
||||||
When Enumer is applied to a type, it will generate:
|
When Enumer is applied to a type, it will generate:
|
||||||
|
|
||||||
* A method `String()` that returns the string representation of the enum value. This makes the enum conform
|
* A method `String()` that returns the string representation of the enum value. This makes the enum conform
|
||||||
|
@ -72,6 +72,28 @@ pillJSON := Aspirin.MarshalJSON()
|
||||||
The generated code is exactly the same as the Stringer tool plus the mentioned additions, so you can use
|
The generated code is exactly the same as the Stringer tool plus the mentioned additions, so you can use
|
||||||
**Enumer** where you are already using **Stringer** without any code change.
|
**Enumer** where you are already using **Stringer** without any code change.
|
||||||
|
|
||||||
|
## Enum value string representation transformation
|
||||||
|
|
||||||
|
Stringer tool uses the same name for enum value string representation (usually CamelCase in Go).
|
||||||
|
|
||||||
|
```go
|
||||||
|
type MyType int
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
name := MyTypeValue.String() // name => "MyTypeValue"
|
||||||
|
```
|
||||||
|
|
||||||
|
Sometimes you need to use some other string representation format then CamelCase (i.e. in JSON).
|
||||||
|
|
||||||
|
To transform enum value string representation from CamelCase to snake_case or kebab-case `transform` flag could be used.
|
||||||
|
|
||||||
|
For example, for `enumer -type=MyType -json -transform=snake` command the next string representation will be generated:
|
||||||
|
|
||||||
|
```go
|
||||||
|
name := MyTypeValue.String() // name => "my_type_value"
|
||||||
|
```
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
The usage of Enumer is the same as Stringer, so you can refer to the [Stringer docs](https://godoc.org/golang.org/x/tools/cmd/stringer)
|
The usage of Enumer is the same as Stringer, so you can refer to the [Stringer docs](https://godoc.org/golang.org/x/tools/cmd/stringer)
|
||||||
for more information.
|
for more information.
|
||||||
|
@ -81,6 +103,10 @@ the JSON related methods will be generated. Similarly if the yaml flag is set to
|
||||||
the YAML related methods will be generated. And if the sql flag is set to true, the Scanner and Valuer interface will
|
the YAML related methods will be generated. And if the sql flag is set to true, the Scanner and Valuer interface will
|
||||||
be implemented to seamlessly use the enum in a database model.
|
be implemented to seamlessly use the enum in a database model.
|
||||||
|
|
||||||
|
For enum string representation transformation `transform` flag was added (i.e. `enumer -type=MyType -json -transform=snake`).
|
||||||
|
Possible values are `snake` and `kebab` for transformation to snake_case and kebab-case accordingly.
|
||||||
|
The default value for `transform` flag is `noop` which means no transformation will be performed.
|
||||||
|
|
||||||
## Inspiring projects
|
## Inspiring projects
|
||||||
* [Stringer](https://godoc.org/golang.org/x/tools/cmd/stringer)
|
* [Stringer](https://godoc.org/golang.org/x/tools/cmd/stringer)
|
||||||
* [jsonenums](https://github.com/campoy/jsonenums)
|
* [jsonenums](https://github.com/campoy/jsonenums)
|
||||||
|
|
|
@ -33,7 +33,7 @@ func TestEndToEnd(t *testing.T) {
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
// Create stringer in temporary directory.
|
// Create stringer in temporary directory.
|
||||||
stringer := filepath.Join(dir, "stringer.exe")
|
stringer := filepath.Join(dir, "stringer.exe")
|
||||||
err = run("go", "build", "-o", stringer, "enumer.go", "sql.go", "stringer.go")
|
err = run("go", "build", "-o", stringer, "enumer.go", "sql.go", "stringer.go", "transformer.go")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("building stringer: %s", err)
|
t.Fatalf("building stringer: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -59,13 +59,20 @@ func TestEndToEnd(t *testing.T) {
|
||||||
}
|
}
|
||||||
// Names are known to be ASCII and long enough.
|
// Names are known to be ASCII and long enough.
|
||||||
typeName := fmt.Sprintf("%c%s", name[0]+'A'-'a', name[1:len(name)-len(".go")])
|
typeName := fmt.Sprintf("%c%s", name[0]+'A'-'a', name[1:len(name)-len(".go")])
|
||||||
stringerCompileAndRun(t, dir, stringer, typeName, name)
|
transformNameMethod := "noop"
|
||||||
|
|
||||||
|
if name == "transform.go" {
|
||||||
|
typeName = "CamelCaseValue"
|
||||||
|
transformNameMethod = "snake"
|
||||||
|
}
|
||||||
|
|
||||||
|
stringerCompileAndRun(t, dir, stringer, typeName, name, transformNameMethod)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// stringerCompileAndRun runs stringer for the named file and compiles and
|
// stringerCompileAndRun runs stringer for the named file and compiles and
|
||||||
// runs the target binary in directory dir. That binary will panic if the String method is incorrect.
|
// runs the target binary in directory dir. That binary will panic if the String method is incorrect.
|
||||||
func stringerCompileAndRun(t *testing.T, dir, stringer, typeName, fileName string) {
|
func stringerCompileAndRun(t *testing.T, dir, stringer, typeName, fileName, transformNameMethod string) {
|
||||||
t.Logf("run: %s %s\n", fileName, typeName)
|
t.Logf("run: %s %s\n", fileName, typeName)
|
||||||
source := filepath.Join(dir, fileName)
|
source := filepath.Join(dir, fileName)
|
||||||
err := copy(source, filepath.Join("testdata", fileName))
|
err := copy(source, filepath.Join("testdata", fileName))
|
||||||
|
@ -74,7 +81,7 @@ func stringerCompileAndRun(t *testing.T, dir, stringer, typeName, fileName strin
|
||||||
}
|
}
|
||||||
stringSource := filepath.Join(dir, typeName+"_string.go")
|
stringSource := filepath.Join(dir, typeName+"_string.go")
|
||||||
// Run stringer in temporary directory.
|
// Run stringer in temporary directory.
|
||||||
err = run(stringer, "-type", typeName, "-output", stringSource, source)
|
err = run(stringer, "-type", typeName, "-output", stringSource, "-transform", transformNameMethod, source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -759,7 +759,7 @@ func runGoldenTest(t *testing.T, test Golden, generateJSON, generateYAML, genera
|
||||||
if len(tokens) != 3 {
|
if len(tokens) != 3 {
|
||||||
t.Fatalf("%s: need type declaration on first line", test.name)
|
t.Fatalf("%s: need type declaration on first line", test.name)
|
||||||
}
|
}
|
||||||
g.generate(tokens[1], generateJSON, generateYAML, generateSQL)
|
g.generate(tokens[1], generateJSON, generateYAML, generateSQL, "noop")
|
||||||
got := string(g.format())
|
got := string(g.format())
|
||||||
if got != test.output {
|
if got != test.output {
|
||||||
t.Errorf("%s: got\n====\n%s====\nexpected\n====%s", test.name, got, test.output)
|
t.Errorf("%s: got\n====\n%s====\nexpected\n====%s", test.name, got, test.output)
|
||||||
|
|
37
stringer.go
37
stringer.go
|
@ -81,11 +81,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
typeNames = flag.String("type", "", "comma-separated list of type names; must be set")
|
typeNames = flag.String("type", "", "comma-separated list of type names; must be set")
|
||||||
sql = flag.Bool("sql", false, "if true, the Scanner and Valuer interface will be implemented.")
|
sql = flag.Bool("sql", false, "if true, the Scanner and Valuer interface will be implemented.")
|
||||||
json = flag.Bool("json", false, "if true, json marshaling methods will be generated. Default: false")
|
json = flag.Bool("json", false, "if true, json marshaling methods will be generated. Default: false")
|
||||||
yaml = flag.Bool("yaml", false, "if true, yaml marshaling methods will be generated. Default: false")
|
yaml = flag.Bool("yaml", false, "if true, yaml marshaling methods will be generated. Default: false")
|
||||||
output = flag.String("output", "", "output file name; default srcdir/<type>_string.go")
|
output = flag.String("output", "", "output file name; default srcdir/<type>_string.go")
|
||||||
|
transformMethod = flag.String("transform", "noop", "enum item name transformation method. Default: noop")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Usage is a replacement usage function for the flags package.
|
// Usage is a replacement usage function for the flags package.
|
||||||
|
@ -122,6 +123,7 @@ func main() {
|
||||||
dir string
|
dir string
|
||||||
g Generator
|
g Generator
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(args) == 1 && isDirectory(args[0]) {
|
if len(args) == 1 && isDirectory(args[0]) {
|
||||||
dir = args[0]
|
dir = args[0]
|
||||||
g.parsePackageDir(args[0])
|
g.parsePackageDir(args[0])
|
||||||
|
@ -147,7 +149,7 @@ func main() {
|
||||||
|
|
||||||
// Run generate for each type.
|
// Run generate for each type.
|
||||||
for _, typeName := range types {
|
for _, typeName := range types {
|
||||||
g.generate(typeName, *json, *yaml, *sql)
|
g.generate(typeName, *json, *yaml, *sql, *transformMethod)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format the output.
|
// Format the output.
|
||||||
|
@ -282,8 +284,24 @@ func (pkg *Package) check(fs *token.FileSet, astFiles []*ast.File) {
|
||||||
pkg.typesPkg = typesPkg
|
pkg.typesPkg = typesPkg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *Generator) transformValueNames(values []Value, transformMethod string) {
|
||||||
|
var transform func(string) string
|
||||||
|
switch transformMethod {
|
||||||
|
case "snake":
|
||||||
|
transform = toSnakeCase
|
||||||
|
case "kebab":
|
||||||
|
transform = toKebabCase
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range values {
|
||||||
|
values[i].name = transform(values[i].name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// generate produces the String method for the named type.
|
// generate produces the String method for the named type.
|
||||||
func (g *Generator) generate(typeName string, includeJSON, includeYAML, includeSQL bool) {
|
func (g *Generator) generate(typeName string, includeJSON, includeYAML, includeSQL bool, transformMethod string) {
|
||||||
values := make([]Value, 0, 100)
|
values := make([]Value, 0, 100)
|
||||||
for _, file := range g.pkg.files {
|
for _, file := range g.pkg.files {
|
||||||
// Set the state for this run of the walker.
|
// Set the state for this run of the walker.
|
||||||
|
@ -298,6 +316,9 @@ func (g *Generator) generate(typeName string, includeJSON, includeYAML, includeS
|
||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
log.Fatalf("no values defined for type %s", typeName)
|
log.Fatalf("no values defined for type %s", typeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g.transformValueNames(values, transformMethod)
|
||||||
|
|
||||||
runs := splitIntoRuns(values)
|
runs := splitIntoRuns(values)
|
||||||
// The decision of which pattern to use depends on the number of
|
// The decision of which pattern to use depends on the number of
|
||||||
// runs in the numbers. If there's only one, it's easy. For more than
|
// runs in the numbers. If there's only one, it's easy. For more than
|
||||||
|
@ -380,7 +401,7 @@ func (g *Generator) format() []byte {
|
||||||
|
|
||||||
// Value represents a declared constant.
|
// Value represents a declared constant.
|
||||||
type Value struct {
|
type Value struct {
|
||||||
name string // The name of the constant.
|
name string // The name of the constant after transformation (i.e. camel case => snake case)
|
||||||
// The value is stored as a bit pattern alone. The boolean tells us
|
// The value is stored as a bit pattern alone. The boolean tells us
|
||||||
// whether to interpret it as an int64 or a uint64; the only place
|
// whether to interpret it as an int64 or a uint64; the only place
|
||||||
// this matters is when sorting.
|
// this matters is when sorting.
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type CamelCaseValue int
|
||||||
|
|
||||||
|
const (
|
||||||
|
CamelCaseValueOne CamelCaseValue = iota
|
||||||
|
CamelCaseValueTwo
|
||||||
|
CamelCaseValueThree
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ck(CamelCaseValueOne, "camel_case_value_one")
|
||||||
|
ck(CamelCaseValueTwo, "camel_case_value_two")
|
||||||
|
ck(CamelCaseValueThree, "camel_case_value_three")
|
||||||
|
ck(-127, "CamelCaseValue(-127)")
|
||||||
|
ck(127, "CamelCaseValue(127)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ck(value CamelCaseValue, str string) {
|
||||||
|
if fmt.Sprint(value) != str {
|
||||||
|
panic("transform.go: " + str)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fatih/camelcase"
|
||||||
|
)
|
||||||
|
|
||||||
|
func transform(src, delim string) string {
|
||||||
|
entries := camelcase.Split(src)
|
||||||
|
if len(entries) <= 1 {
|
||||||
|
return strings.ToLower(src)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := strings.ToLower(entries[0])
|
||||||
|
for i := 1; i < len(entries); i++ {
|
||||||
|
result += delim + strings.ToLower(entries[i])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSnakeCase(src string) string {
|
||||||
|
return transform(src, "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
func toKebabCase(src string) string {
|
||||||
|
return transform(src, "-")
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTransform(t *testing.T) {
|
||||||
|
result := transform("CamelCaseStringValue", ".")
|
||||||
|
|
||||||
|
const expected = "camel.case.string.value"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("\ngot: %s\n====\nexpected: %s", result, expected)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
language: go
|
||||||
|
go: 1.4
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Fatih Arslan
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,58 @@
|
||||||
|
# CamelCase [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/fatih/camelcase) [![Build Status](http://img.shields.io/travis/fatih/camelcase.svg?style=flat-square)](https://travis-ci.org/fatih/camelcase)
|
||||||
|
|
||||||
|
CamelCase is a Golang (Go) package to split the words of a camelcase type
|
||||||
|
string into a slice of words. It can be used to convert a camelcase word (lower
|
||||||
|
or upper case) into any type of word.
|
||||||
|
|
||||||
|
## Splitting rules:
|
||||||
|
|
||||||
|
1. If string is not valid UTF-8, return it without splitting as
|
||||||
|
single item array.
|
||||||
|
2. Assign all unicode characters into one of 4 sets: lower case
|
||||||
|
letters, upper case letters, numbers, and all other characters.
|
||||||
|
3. Iterate through characters of string, introducing splits
|
||||||
|
between adjacent characters that belong to different sets.
|
||||||
|
4. Iterate through array of split strings, and if a given string
|
||||||
|
is upper case:
|
||||||
|
* if subsequent string is lower case:
|
||||||
|
* move last character of upper case string to beginning of
|
||||||
|
lower case string
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/fatih/camelcase
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage and examples
|
||||||
|
|
||||||
|
```go
|
||||||
|
splitted := camelcase.Split("GolangPackage")
|
||||||
|
|
||||||
|
fmt.Println(splitted[0], splitted[1]) // prints: "Golang", "Package"
|
||||||
|
```
|
||||||
|
|
||||||
|
Both lower camel case and upper camel case are supported. For more info please
|
||||||
|
check: [http://en.wikipedia.org/wiki/CamelCase](http://en.wikipedia.org/wiki/CamelCase)
|
||||||
|
|
||||||
|
Below are some example cases:
|
||||||
|
|
||||||
|
```
|
||||||
|
"" => []
|
||||||
|
"lowercase" => ["lowercase"]
|
||||||
|
"Class" => ["Class"]
|
||||||
|
"MyClass" => ["My", "Class"]
|
||||||
|
"MyC" => ["My", "C"]
|
||||||
|
"HTML" => ["HTML"]
|
||||||
|
"PDFLoader" => ["PDF", "Loader"]
|
||||||
|
"AString" => ["A", "String"]
|
||||||
|
"SimpleXMLParser" => ["Simple", "XML", "Parser"]
|
||||||
|
"vimRPCPlugin" => ["vim", "RPC", "Plugin"]
|
||||||
|
"GL11Version" => ["GL", "11", "Version"]
|
||||||
|
"99Bottles" => ["99", "Bottles"]
|
||||||
|
"May5" => ["May", "5"]
|
||||||
|
"BFG9000" => ["BFG", "9000"]
|
||||||
|
"BöseÜberraschung" => ["Böse", "Überraschung"]
|
||||||
|
"Two spaces" => ["Two", " ", "spaces"]
|
||||||
|
"BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"]
|
||||||
|
```
|
|
@ -0,0 +1,90 @@
|
||||||
|
// Package camelcase is a micro package to split the words of a camelcase type
|
||||||
|
// string into a slice of words.
|
||||||
|
package camelcase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Split splits the camelcase word and returns a list of words. It also
|
||||||
|
// supports digits. Both lower camel case and upper camel case are supported.
|
||||||
|
// For more info please check: http://en.wikipedia.org/wiki/CamelCase
|
||||||
|
//
|
||||||
|
// Examples
|
||||||
|
//
|
||||||
|
// "" => [""]
|
||||||
|
// "lowercase" => ["lowercase"]
|
||||||
|
// "Class" => ["Class"]
|
||||||
|
// "MyClass" => ["My", "Class"]
|
||||||
|
// "MyC" => ["My", "C"]
|
||||||
|
// "HTML" => ["HTML"]
|
||||||
|
// "PDFLoader" => ["PDF", "Loader"]
|
||||||
|
// "AString" => ["A", "String"]
|
||||||
|
// "SimpleXMLParser" => ["Simple", "XML", "Parser"]
|
||||||
|
// "vimRPCPlugin" => ["vim", "RPC", "Plugin"]
|
||||||
|
// "GL11Version" => ["GL", "11", "Version"]
|
||||||
|
// "99Bottles" => ["99", "Bottles"]
|
||||||
|
// "May5" => ["May", "5"]
|
||||||
|
// "BFG9000" => ["BFG", "9000"]
|
||||||
|
// "BöseÜberraschung" => ["Böse", "Überraschung"]
|
||||||
|
// "Two spaces" => ["Two", " ", "spaces"]
|
||||||
|
// "BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"]
|
||||||
|
//
|
||||||
|
// Splitting rules
|
||||||
|
//
|
||||||
|
// 1) If string is not valid UTF-8, return it without splitting as
|
||||||
|
// single item array.
|
||||||
|
// 2) Assign all unicode characters into one of 4 sets: lower case
|
||||||
|
// letters, upper case letters, numbers, and all other characters.
|
||||||
|
// 3) Iterate through characters of string, introducing splits
|
||||||
|
// between adjacent characters that belong to different sets.
|
||||||
|
// 4) Iterate through array of split strings, and if a given string
|
||||||
|
// is upper case:
|
||||||
|
// if subsequent string is lower case:
|
||||||
|
// move last character of upper case string to beginning of
|
||||||
|
// lower case string
|
||||||
|
func Split(src string) (entries []string) {
|
||||||
|
// don't split invalid utf8
|
||||||
|
if !utf8.ValidString(src) {
|
||||||
|
return []string{src}
|
||||||
|
}
|
||||||
|
entries = []string{}
|
||||||
|
var runes [][]rune
|
||||||
|
lastClass := 0
|
||||||
|
class := 0
|
||||||
|
// split into fields based on class of unicode character
|
||||||
|
for _, r := range src {
|
||||||
|
switch true {
|
||||||
|
case unicode.IsLower(r):
|
||||||
|
class = 1
|
||||||
|
case unicode.IsUpper(r):
|
||||||
|
class = 2
|
||||||
|
case unicode.IsDigit(r):
|
||||||
|
class = 3
|
||||||
|
default:
|
||||||
|
class = 4
|
||||||
|
}
|
||||||
|
if class == lastClass {
|
||||||
|
runes[len(runes)-1] = append(runes[len(runes)-1], r)
|
||||||
|
} else {
|
||||||
|
runes = append(runes, []rune{r})
|
||||||
|
}
|
||||||
|
lastClass = class
|
||||||
|
}
|
||||||
|
// handle upper case -> lower case sequences, e.g.
|
||||||
|
// "PDFL", "oader" -> "PDF", "Loader"
|
||||||
|
for i := 0; i < len(runes)-1; i++ {
|
||||||
|
if unicode.IsUpper(runes[i][0]) && unicode.IsLower(runes[i+1][0]) {
|
||||||
|
runes[i+1] = append([]rune{runes[i][len(runes[i])-1]}, runes[i+1]...)
|
||||||
|
runes[i] = runes[i][:len(runes[i])-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// construct []string from results
|
||||||
|
for _, s := range runes {
|
||||||
|
if len(s) > 0 {
|
||||||
|
entries = append(entries, string(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package camelcase
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func ExampleSplit() {
|
||||||
|
|
||||||
|
for _, c := range []string{
|
||||||
|
"",
|
||||||
|
"lowercase",
|
||||||
|
"Class",
|
||||||
|
"MyClass",
|
||||||
|
"MyC",
|
||||||
|
"HTML",
|
||||||
|
"PDFLoader",
|
||||||
|
"AString",
|
||||||
|
"SimpleXMLParser",
|
||||||
|
"vimRPCPlugin",
|
||||||
|
"GL11Version",
|
||||||
|
"99Bottles",
|
||||||
|
"May5",
|
||||||
|
"BFG9000",
|
||||||
|
"BöseÜberraschung",
|
||||||
|
"Two spaces",
|
||||||
|
"BadUTF8\xe2\xe2\xa1",
|
||||||
|
} {
|
||||||
|
fmt.Printf("%#v => %#v\n", c, Split(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// "" => []string{}
|
||||||
|
// "lowercase" => []string{"lowercase"}
|
||||||
|
// "Class" => []string{"Class"}
|
||||||
|
// "MyClass" => []string{"My", "Class"}
|
||||||
|
// "MyC" => []string{"My", "C"}
|
||||||
|
// "HTML" => []string{"HTML"}
|
||||||
|
// "PDFLoader" => []string{"PDF", "Loader"}
|
||||||
|
// "AString" => []string{"A", "String"}
|
||||||
|
// "SimpleXMLParser" => []string{"Simple", "XML", "Parser"}
|
||||||
|
// "vimRPCPlugin" => []string{"vim", "RPC", "Plugin"}
|
||||||
|
// "GL11Version" => []string{"GL", "11", "Version"}
|
||||||
|
// "99Bottles" => []string{"99", "Bottles"}
|
||||||
|
// "May5" => []string{"May", "5"}
|
||||||
|
// "BFG9000" => []string{"BFG", "9000"}
|
||||||
|
// "BöseÜberraschung" => []string{"Böse", "Überraschung"}
|
||||||
|
// "Two spaces" => []string{"Two", " ", "spaces"}
|
||||||
|
// "BadUTF8\xe2\xe2\xa1" => []string{"BadUTF8\xe2\xe2\xa1"}
|
||||||
|
}
|
Loading…
Reference in New Issue