Skip to content

Instantly share code, notes, and snippets.

@stoewer
Last active March 29, 2022 16:59
Show Gist options
  • Select an option

  • Save stoewer/fbe273b711e6a06315d19552dd4d33e6 to your computer and use it in GitHub Desktop.

Select an option

Save stoewer/fbe273b711e6a06315d19552dd4d33e6 to your computer and use it in GitHub Desktop.
Convert camel case to snake case in Go http://play.golang.org/p/9ybqJat1Hr
import (
"fmt"
"strings"
"regexp"
)
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
func ToSnakeCase(str string) string {
snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
return strings.ToLower(snake)
}
@quasilyte

quasilyte commented Jan 30, 2020

Copy link
Copy Markdown

Tests:

func TestToSnakeCase(t *testing.T) {
	tests := []struct {
		input string
		want  string
	}{
		{"", ""},
		{"already_snake", "already_snake"},
		{"A", "a"},
		{"AA", "aa"},
		{"AaAa", "aa_aa"},
		{"HTTPRequest", "http_request"},
		{"BatteryLifeValue", "battery_life_value"},
		{"Id0Value", "id0_value"},
		{"ID0Value", "id0_value"},
	}
	for _, test := range tests {
		have := ToSnakeCase(test.input)
		if have != test.want {
			t.Errorf("input=%q:\nhave: %q\nwant: %q", test.input, have, test.want)
		}
	}
}

@adzimzf

adzimzf commented Jul 8, 2020

Copy link
Copy Markdown

Thanks buddy this solved my problem

@DuckSoft

Copy link
Copy Markdown

Nice code! What is this code licensed in?

@stoewer

stoewer commented Sep 30, 2020

Copy link
Copy Markdown
Author

It's not licensed at all, but feel free to use it. If you are using go you could also use https://github.com/stoewer/go-strcase, which is licensed under MIT License.

@arunshaji95

Copy link
Copy Markdown

Fails for the input already_Snake

@JensSkipr

Copy link
Copy Markdown

Fails for the input already_Snake

Changing the first regex to ([A-Z])([A-Z][a-z]) seems to solve the issue:

import (
	"fmt"
	"strings"
	"regexp"
)

var matchFirstCap = regexp.MustCompile("([A-Z])([A-Z][a-z])")
var matchAllCap   = regexp.MustCompile("([a-z0-9])([A-Z])")

func ToSnakeCase(str string) string {
	snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
	snake  = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
	return strings.ToLower(snake)
}

After changing to our projects guidelines and requirements, I have following function:

var matchFirstCap = regexp.MustCompile("([A-Z])([A-Z][a-z])")
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")

// ToSnakeCase converts the provided string to snake_case.
// Based on https://gist.github.com/stoewer/fbe273b711e6a06315d19552dd4d33e6
func ToSnakeCase(input string) string {
	output := matchFirstCap.ReplaceAllString(input, "${1}_${2}")
	output = matchAllCap.ReplaceAllString(output, "${1}_${2}")
	output = strings.ReplaceAll(output, "-", "_")
	return strings.ToLower(output)
}

This function passes following (extended) test:

import (
	"testing"
	"github.com/stretchr/testify/assert"
)

func Test_ToSnakeCase(t *testing.T) {
	// Based on https://gist.github.com/stoewer/fbe273b711e6a06315d19552dd4d33e6
	tests := []struct {
		input    string
		expected string
	}{
		{"", ""},
		{"camelCase", "camel_case"},
		{"PascalCase", "pascal_case"},
		{"snake_case", "snake_case"},
		{"Pascal_Snake", "pascal_snake"},
		{"SCREAMING_SNAKE", "screaming_snake"},
		{"kebab-case", "kebab_case"},
		{"Pascal-Kebab", "pascal_kebab"},
		{"SCREAMING-KEBAB", "screaming_kebab"},
		{"A", "a"},
		{"AA", "aa"},
		{"AAA", "aaa"},
		{"AAAA", "aaaa"},
		{"AaAa", "aa_aa"},
		{"HTTPRequest", "http_request"},
		{"BatteryLifeValue", "battery_life_value"},
		{"Id0Value", "id0_value"},
		{"ID0Value", "id0_value"},
	}

	for _, test := range tests {
		result := ToSnakeCase(test.input)
		assert.Equal(t, test.expected, result)
	}
}

@porfirion

Copy link
Copy Markdown

Jen's function is awesome and covers a lot of different cases. But using regexp when you just need to convert uppercase letters into '_' plus lowercase letter - is overkill.

Here is very simple and much faster solution:

func toSnakeCase(s string) string {
   var res = make([]rune, 0, len(s))
   for i, r := range s {
       if unicode.IsUpper(r) && i > 0 {
           res = append(res, '_', unicode.ToLower(r))
       } else {
           res = append(res, unicode.ToLower(r))
       }
   }
   return string(res)
}

And here is benchmark for Jen's variant and mine:

BenchmarkRegexp-8   	   92520	     11980 ns/op	    1061 B/op	      59 allocs/op
BenchmarkRange-8    	  609658	      1807 ns/op	     744 B/op	      20 allocs/op

6 times faster and 3 times less allocations!

@JensSkipr

Copy link
Copy Markdown

@porfirion thanks for the proposal and comparison! I noticed most of my unit test fail when using your version. Therefore, I tried to make a version which succeeds (nearly) all tests. It can still use some cleanup and unit test for HTTPRequest still fails, but nearly same performance improvements.

Code

func ToSnakeCase(s string) string {
	var res = make([]rune, 0, len(s))
	var p = '_'
	for i, r := range s {
		if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
			res = append(res, '_')
		} else if unicode.IsUpper(r) && i > 0 {
			if unicode.IsLetter(p) && !unicode.IsUpper(p) || unicode.IsDigit(p) {
				res = append(res, '_', unicode.ToLower(r))
			} else {
				res = append(res, unicode.ToLower(r))
			}
		} else {
			res = append(res, unicode.ToLower(r))
		}

		p = r
	}
	return string(res)
}

Benchmarks

func BenchmarkToSnakeCaseRegex(b *testing.B) {
	for i := 0; i < b.N; i++ {
		ToSnakeCaseRegex("BatteryLifeValue")
	}
}

func BenchmarkToSnakeCase(b *testing.B) {
	for i := 0; i < b.N; i++ {
		ToSnakeCase("BatteryLifeValue")
	}
}

Results

BenchmarkToSnakeCaseRegex-12    	  360540	      3282 ns/op	     282 B/op	      11 allocs/op
BenchmarkToSnakeCase-12         	 1726478	       714 ns/op	     224 B/op	       3 allocs/op

@hxsf

hxsf commented Sep 17, 2021

Copy link
Copy Markdown

@JensSkipr @porfirion

Hi, I make one more, but it pass all test, and better performance.

source file in here: https://gist.github.com/hxsf/7f5392c0153d3a8607c42eefed02b8cd

Bench

> go test -benchmem -bench 'One'
goos: darwin
goarch: amd64
pkg: mesh-sidecar/cmd/vars
cpu: Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz
BenchmarkOneCase/ToSnakeCaseRegex-8               490156              2403 ns/op             305 B/op         12 allocs/op
BenchmarkOneCase/ToSnakeCaseByJensSkipr-8        4163742               286.1 ns/op           216 B/op          3 allocs/op
BenchmarkOneCase/ToSnakeCase-8                   8867581               131.4 ns/op            24 B/op          1 allocs/op

BenchmarkAllInOne/ToSnakeCaseRegex-8               51538             22480 ns/op            2307 B/op        137 allocs/op
BenchmarkAllInOne/ToSnakeCaseByJensSkipr-8        395026              3051 ns/op            1352 B/op         41 allocs/op
BenchmarkAllInOne/ToSnakeCase-8                   857940              1491 ns/op             232 B/op         17 allocs/op
PASS
ok      mesh-sidecar/cmd/vars   7.930s

@porfirion

Copy link
Copy Markdown

@hxsf looks awesome! Yeah, I forgot about additional capacity for underscores %)

@adombeck

Copy link
Copy Markdown

@hxsf: That looks great! Do you mind adding a license to your gist?

@hxsf

hxsf commented Sep 27, 2021

Copy link
Copy Markdown

@adombeck add MIT license.

@adombeck

Copy link
Copy Markdown

@hxsf: Great, thanks a lot!

@elvizlai

elvizlai commented Oct 16, 2021

Copy link
Copy Markdown

@hxsf Test failed for

MyLIFEIsAwesomE should be my_life_is_awesom_e but got mylife_is_awesome
Japan125Canada130Australia150 should be japan125_canada130_australia150 but got japan1_2_5_canada1_3_0_australia1_5_0

@hxsf

hxsf commented Oct 16, 2021

Copy link
Copy Markdown

@elvizlai I update the code. and it works

@abdennour

Copy link
Copy Markdown

good guys ! keep it up!
kebab case should be concluded here as well

@hxsf

hxsf commented Jan 18, 2022

Copy link
Copy Markdown

@abdennour just replace '_' with '-' in the code, and change the function name. I think it will work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment