├── .circleci └── config.yml ├── .gitignore ├── History.md ├── Readme.md ├── benchmarks_test.go ├── snake.go └── snake_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: /go/src/github.com/segmentio/go-snakecase 5 | docker: 6 | - image: circleci/golang 7 | steps: 8 | - checkout 9 | - setup_remote_docker: { reusable: true, docker_layer_caching: true } 10 | - run: go get -v -t ./... 11 | - run: go vet ./... 12 | - run: go test -v -race ./... 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # Emacs 27 | *~ 28 | *.txt -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | v1.1.0 / 2018-11-01 3 | =================== 4 | 5 | * use circleci 2.0 6 | * [hot fix] CIRCLE => CIRCLECI 7 | * [hot fix] properly pass environment variables to build container 8 | * update circle.yml 9 | * update circle.yml 10 | * cleanup 11 | * remove .travis.yml 12 | * remove unneeded toLower call 13 | * add .gitignore 14 | * add circle.yml 15 | * really fast snakecase 16 | * fix isUpper typo 17 | * add travis 18 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # go-snakecase 3 | 4 | > **Note** 5 | > Segment has paused maintenance on this project, but may return it to an active status in the future. Issues and pull requests from external contributors are not being considered, although internal contributions may appear from time to time. The project remains available under its open source license for anyone to use. 6 | 7 | [![Build Status](https://travis-ci.org/segmentio/go-snakecase.svg?branch=master)](https://travis-ci.org/segmentio/go-snakecase) 8 | 9 | Fast snakecase implementation, believe it or not this was a large bottleneck in our application, Go's regexps are very slow. 10 | 11 | # License 12 | 13 | MIT 14 | -------------------------------------------------------------------------------- /benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package snakecase 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func BenchmarkUnchangedLong(b *testing.B) { 8 | var s = "invite_your_customers_add_invites" 9 | b.SetBytes(int64(len(s))) 10 | for n := 0; n < b.N; n++ { 11 | Snakecase(s) 12 | } 13 | } 14 | 15 | func BenchmarkUnchangedSimple(b *testing.B) { 16 | var s = "sample_text" 17 | b.SetBytes(int64(len(s))) 18 | for n := 0; n < b.N; n++ { 19 | Snakecase(s) 20 | } 21 | } 22 | 23 | func BenchmarkModifiedUnicode(b *testing.B) { 24 | var s = "ß_ƒ_foo" 25 | b.SetBytes(int64(len(s))) 26 | for n := 0; n < b.N; n++ { 27 | Snakecase(s) 28 | } 29 | } 30 | func BenchmarkModifiedLong(b *testing.B) { 31 | var s = "inviteYourCustomersAddInvites" 32 | b.SetBytes(int64(len(s))) 33 | for n := 0; n < b.N; n++ { 34 | Snakecase(s) 35 | } 36 | } 37 | 38 | func BenchmarkModifiedLongSpecialChars(b *testing.B) { 39 | var s = "FOO:BAR$BAZ__Sample Text___" 40 | b.SetBytes(int64(len(s))) 41 | for n := 0; n < b.N; n++ { 42 | Snakecase(s) 43 | } 44 | } 45 | 46 | func BenchmarkModifiedSimple(b *testing.B) { 47 | var s = "sample text" 48 | b.SetBytes(int64(len(s))) 49 | for n := 0; n < b.N; n++ { 50 | Snakecase("sample text") 51 | } 52 | } 53 | 54 | func BenchmarkModifiedUnicode2(b *testing.B) { 55 | var s = "ẞ•¶§ƒ˚foo˙∆˚¬" 56 | b.SetBytes(int64(len(s))) 57 | for n := 0; n < b.N; n++ { 58 | Snakecase(s) 59 | } 60 | } 61 | 62 | func BenchmarkLeadingUnderscoresDigitUpper(b *testing.B) { 63 | var s = "_5TEst" 64 | b.SetBytes(int64(len(s))) 65 | for n := 0; n < b.N; n++ { 66 | Snakecase(s) 67 | } 68 | } 69 | 70 | func BenchmarkDigitUpper(b *testing.B) { 71 | var s = "5TEst" 72 | b.SetBytes(int64(len(s))) 73 | for n := 0; n < b.N; n++ { 74 | Snakecase(s) 75 | } 76 | } 77 | 78 | func BenchmarkDigitUpper2(b *testing.B) { 79 | var s = "lk0B@bFmjrLQ_Z6YL" 80 | b.SetBytes(int64(len(s))) 81 | for n := 0; n < b.N; n++ { 82 | Snakecase(s) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /snake.go: -------------------------------------------------------------------------------- 1 | // Package snakecase - Super-Fast snake-case implementation. 2 | package snakecase 3 | 4 | const underscoreByte = '_' 5 | 6 | // Snakecase the given string. 7 | func Snakecase(s string) string { 8 | idx := 0 9 | hasLower := false 10 | hasUnderscore := false 11 | lowercaseSinceUnderscore := false 12 | 13 | // loop through all good characters: 14 | // - lowercase 15 | // - digit 16 | // - underscore (as long as the next character is lowercase or digit) 17 | for ; idx < len(s); idx++ { 18 | if isLower(s[idx]) { 19 | hasLower = true 20 | if hasUnderscore { 21 | lowercaseSinceUnderscore = true 22 | } 23 | continue 24 | } else if isDigit(s[idx]) { 25 | continue 26 | } else if s[idx] == underscoreByte && idx > 0 && idx < len(s)-1 && (isLower(s[idx+1]) || isDigit(s[idx+1])) { 27 | hasUnderscore = true 28 | lowercaseSinceUnderscore = false 29 | continue 30 | } 31 | break 32 | } 33 | 34 | if idx == len(s) { 35 | return s // no changes needed, can just borrow the string 36 | } 37 | 38 | // if we get here then we must need to manipulate the string 39 | b := make([]byte, 0, 64) 40 | b = append(b, s[:idx]...) 41 | 42 | if isUpper(s[idx]) && (!hasLower || hasUnderscore && !lowercaseSinceUnderscore) { 43 | for idx < len(s) && (isUpper(s[idx]) || isDigit(s[idx])) { 44 | b = append(b, asciiLowercaseArray[s[idx]]) 45 | idx++ 46 | } 47 | 48 | for idx < len(s) && (isLower(s[idx]) || isDigit(s[idx])) { 49 | b = append(b, s[idx]) 50 | idx++ 51 | } 52 | } 53 | 54 | for idx < len(s) { 55 | if !isAlphanumeric(s[idx]) { 56 | idx++ 57 | continue 58 | } 59 | 60 | if len(b) > 0 { 61 | b = append(b, underscoreByte) 62 | } 63 | 64 | for idx < len(s) && (isUpper(s[idx]) || isDigit(s[idx])) { 65 | b = append(b, asciiLowercaseArray[s[idx]]) 66 | idx++ 67 | } 68 | 69 | for idx < len(s) && (isLower(s[idx]) || isDigit(s[idx])) { 70 | b = append(b, s[idx]) 71 | idx++ 72 | } 73 | } 74 | return string(b) // return manipulated string 75 | } 76 | 77 | func isAlphanumeric(c byte) bool { 78 | return isLower(c) || isUpper(c) || isDigit(c) 79 | } 80 | 81 | func isUpper(c byte) bool { 82 | return c >= 'A' && c <= 'Z' 83 | } 84 | 85 | func isLower(c byte) bool { 86 | return c >= 'a' && c <= 'z' 87 | } 88 | 89 | func isDigit(c byte) bool { 90 | return c >= '0' && c <= '9' 91 | } 92 | 93 | var asciiLowercaseArray = [256]byte{ 94 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 95 | 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 96 | 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 97 | 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 98 | ' ', '!', '"', '#', '$', '%', '&', '\'', 99 | '(', ')', '*', '+', ',', '-', '.', '/', 100 | '0', '1', '2', '3', '4', '5', '6', '7', 101 | '8', '9', ':', ';', '<', '=', '>', '?', 102 | '@', 103 | 104 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 105 | 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 106 | 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 107 | 'x', 'y', 'z', 108 | 109 | '[', '\\', ']', '^', '_', 110 | '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 111 | 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 112 | 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 113 | 'x', 'y', 'z', '{', '|', '}', '~', 0x7f, 114 | 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 115 | 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 116 | 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 117 | 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 118 | 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 119 | 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 120 | 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 121 | 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 122 | 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 123 | 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, 124 | 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 125 | 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, 126 | 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 127 | 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 128 | 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 129 | 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, 130 | } 131 | -------------------------------------------------------------------------------- /snake_test.go: -------------------------------------------------------------------------------- 1 | package snakecase 2 | 3 | import "testing" 4 | 5 | var ops int = 1e6 6 | 7 | type sample struct { 8 | str, out string 9 | } 10 | 11 | func TestSnakecase(t *testing.T) { 12 | samples := []sample{ 13 | {"@49L0S145_¬fwHƒ0TSLNVp", "49l0s145_fw_h_0tslnvp"}, 14 | {"lk0B@bFmjrLQ_Z6YL", "lk0_b_b_fmjr_lq_z6yl"}, 15 | {"samPLE text", "sam_ple_text"}, 16 | {"sample text", "sample_text"}, 17 | {"sample-text", "sample_text"}, 18 | {"sample_text", "sample_text"}, 19 | {"sample___text", "sample_text"}, 20 | {"sampleText", "sample_text"}, 21 | {"inviteYourCustomersAddInvites", "invite_your_customers_add_invites"}, 22 | {"sample 2 Text", "sample_2_text"}, 23 | {" sample 2 Text ", "sample_2_text"}, 24 | {" $#$sample 2 Text ", "sample_2_text"}, 25 | {"SAMPLE 2 TEXT", "sample_2_text"}, 26 | {"___$$Base64Encode", "base64_encode"}, 27 | {"FOO:BAR$BAZ", "foo_bar_baz"}, 28 | {"FOO#BAR#BAZ", "foo_bar_baz"}, 29 | {"something.com", "something_com"}, 30 | {"$something%", "something"}, 31 | {"something.com", "something_com"}, 32 | {"•¶§ƒ˚foo˙∆˚¬", "foo"}, 33 | {"CStringRef", "cstring_ref"}, 34 | {"5test", "5test"}, 35 | {"test5", "test5"}, 36 | {"THE5r", "the5r"}, 37 | {"5TEst", "5test"}, 38 | {"_5TEst", "5test"}, 39 | {"@%#&5TEst", "5test"}, 40 | {"edf_6N", "edf_6n"}, 41 | {"f_pX9", "f_p_x9"}, 42 | {"p_z9Rg", "p_z9_rg"}, 43 | {"2FA Enabled", "2fa_enabled"}, 44 | {"Enabled 2FA", "enabled_2fa"}, 45 | } 46 | 47 | for _, sample := range samples { 48 | if out := Snakecase(sample.str); out != sample.out { 49 | t.Errorf("got %q from %q, expected %q", out, sample.str, sample.out) 50 | } 51 | } 52 | } 53 | --------------------------------------------------------------------------------