├── img ├── sf_cover.png ├── benchmarks.png ├── benchmarks2.png ├── benchmarks_adv.png ├── map2str_benchmarks.png └── slice2str_benchmarks.png ├── .idea ├── misc.xml ├── vcs.xml ├── .gitignore ├── modules.xml └── stringFormatter.iml ├── go.mod ├── .gitignore ├── utils └── run_benchamrks.ps1 ├── .github ├── ISSUE_TEMPLATE │ └── feature_request.md └── workflows │ └── ci.yml ├── maptostring_benchmark_test.go ├── slicetostring_test.go ├── slicetostring.go ├── maptostring.go ├── go.sum ├── maptostring_test.go ├── .golangci.yaml ├── slicetostring_benchmark_test.go ├── formatter_benchmark_test.go ├── CODE_OF_CONDUCT.md ├── stringstyle_formatter_test.go ├── stringstyle_formatter.go ├── README.md ├── LICENSE ├── formatter_test.go └── formatter.go /img/sf_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wissance/stringFormatter/HEAD/img/sf_cover.png -------------------------------------------------------------------------------- /img/benchmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wissance/stringFormatter/HEAD/img/benchmarks.png -------------------------------------------------------------------------------- /img/benchmarks2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wissance/stringFormatter/HEAD/img/benchmarks2.png -------------------------------------------------------------------------------- /img/benchmarks_adv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wissance/stringFormatter/HEAD/img/benchmarks_adv.png -------------------------------------------------------------------------------- /img/map2str_benchmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wissance/stringFormatter/HEAD/img/map2str_benchmarks.png -------------------------------------------------------------------------------- /img/slice2str_benchmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wissance/stringFormatter/HEAD/img/slice2str_benchmarks.png -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wissance/stringFormatter 2 | 3 | go 1.18 4 | 5 | require github.com/stretchr/testify v1.8.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /.idea/stringFormatter.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /utils/run_benchamrks.ps1: -------------------------------------------------------------------------------- 1 | $root_dir = Resolve-Path -Path ".." 2 | echo "******** 1. standard fmt formatting lib benchmarks ******** " 3 | go test $root_dir -bench=Fmt -benchmem -cpu 1 4 | echo "******** 2. stringFormatter lib benchmarks ******** " 5 | go test $root_dir -bench=Format -benchmem -cpu 1 6 | echo "******** 3. slice fmt benchmarks ******** " 7 | go test $root_dir -bench=SliceStandard -benchmem -cpu 1 8 | echo "******** 4. stringFormatter lib benchmarks ******** " 9 | go test $root_dir -bench=SliceToStringAdvanced -benchmem -cpu 1 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /maptostring_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package stringFormatter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wissance/stringFormatter" 7 | ) 8 | 9 | func BenchmarkMapToStringWith11Keys(b *testing.B) { 10 | optionsMap := map[string]any{ 11 | "timeoutMS": 2000, 12 | "connectTimeoutMS": 20000, 13 | "maxPoolSize": 64, 14 | "replicaSet": "main-set", 15 | "maxIdleTimeMS": 30000, 16 | "socketTimeoutMS": 400, 17 | "serverSelectionTimeoutMS": 2000, 18 | "heartbeatFrequencyMS": 20, 19 | "tls": "certs/my_cert.crt", 20 | "w": true, 21 | "directConnection": false, 22 | } 23 | 24 | for i := 0; i < b.N; i++ { 25 | _ = stringFormatter.MapToString(optionsMap, "{key} : {value}", ", ") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /slicetostring_test.go: -------------------------------------------------------------------------------- 1 | package stringFormatter_test 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/wissance/stringFormatter" 6 | "testing" 7 | ) 8 | 9 | func TestSliceToString(t *testing.T) { 10 | for name, test := range map[string]struct { 11 | separator string 12 | data []any 13 | expectedResult string 14 | }{ 15 | "comma-separated slice": { 16 | separator: ", ", 17 | data: []any{11, 22, 33, 44, 55, 66, 77, 88, 99}, 18 | expectedResult: "11, 22, 33, 44, 55, 66, 77, 88, 99", 19 | }, 20 | "dash(kebab) line from slice": { 21 | separator: "-", 22 | data: []any{"str1", "str2", 101, "str3"}, 23 | expectedResult: "str1-str2-101-str3", 24 | }, 25 | } { 26 | t.Run(name, func(t *testing.T) { 27 | actualResult := stringFormatter.SliceToString(&test.data, &test.separator) 28 | assert.Equal(t, test.expectedResult, actualResult) 29 | }) 30 | } 31 | } 32 | 33 | func TestSliceSameTypeToString(t *testing.T) { 34 | separator := ":" 35 | numericSlice := []int{100, 200, 400, 800} 36 | result := stringFormatter.SliceSameTypeToString(&numericSlice, &separator) 37 | assert.Equal(t, "100:200:400:800", result) 38 | } 39 | -------------------------------------------------------------------------------- /slicetostring.go: -------------------------------------------------------------------------------- 1 | package stringFormatter 2 | 3 | import "strings" 4 | 5 | // SliceToString function that converts slice of any type items to string in format {item}{sep}{item}... 6 | // TODO(UMV): probably add one more param to wrap item in quotes if necessary 7 | func SliceToString(data *[]any, separator *string) string { 8 | if len(*data) == 0 { 9 | return "" 10 | } 11 | 12 | sliceStr := &strings.Builder{} 13 | // init memory initially 14 | sliceStr.Grow(len(*data)*len(*separator)*2 + (len(*data)-1)*len(*separator)) 15 | isFirst := true 16 | for _, item := range *data { 17 | if !isFirst { 18 | sliceStr.WriteString(*separator) 19 | } 20 | sliceStr.WriteString(Format("{0}", item)) 21 | isFirst = false 22 | } 23 | 24 | return sliceStr.String() 25 | } 26 | 27 | func SliceSameTypeToString[T any](data *[]T, separator *string) string { 28 | if len(*data) == 0 { 29 | return "" 30 | } 31 | 32 | sliceStr := &strings.Builder{} 33 | // init memory initially 34 | sliceStr.Grow(len(*data)*len(*separator)*2 + (len(*data)-1)*len(*separator)) 35 | isFirst := true 36 | for _, item := range *data { 37 | if !isFirst { 38 | sliceStr.WriteString(*separator) 39 | } 40 | sliceStr.WriteString(Format("{0}", item)) 41 | isFirst = false 42 | } 43 | 44 | return sliceStr.String() 45 | } 46 | -------------------------------------------------------------------------------- /maptostring.go: -------------------------------------------------------------------------------- 1 | package stringFormatter 2 | 3 | import "strings" 4 | 5 | const ( 6 | // KeyKey placeholder will be formatted to map key 7 | KeyKey = "key" 8 | // KeyValue placeholder will be formatted to map value 9 | KeyValue = "value" 10 | ) 11 | 12 | // MapToString - format map keys and values according to format, joining parts with separator. 13 | // Format should contain key and value placeholders which will be used for formatting, e.g. 14 | // "{key} : {value}", or "{value}", or "{key} => {value}". 15 | // Parts order in resulting string is not guranteed. 16 | func MapToString[ 17 | K string | int | uint | int32 | int64 | uint32 | uint64, 18 | V any, 19 | ](data map[K]V, format string, separator string) string { 20 | if len(data) == 0 { 21 | return "" 22 | } 23 | 24 | mapStr := &strings.Builder{} 25 | // assuming format will be at most two times larger after formatting part, 26 | // plus exact number of bytes for separators 27 | mapStr.Grow(len(data)*len(format)*2 + (len(data)-1)*len(separator)) 28 | 29 | isFirst := true 30 | for k, v := range data { 31 | if !isFirst { 32 | mapStr.WriteString(separator) 33 | } 34 | 35 | line := FormatComplex(string(format), map[string]any{ 36 | KeyKey: k, 37 | KeyValue: v, 38 | }) 39 | mapStr.WriteString(line) 40 | isFirst = false 41 | } 42 | 43 | return mapStr.String() 44 | } 45 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 9 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 10 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 13 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 14 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 15 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | -------------------------------------------------------------------------------- /maptostring_test.go: -------------------------------------------------------------------------------- 1 | package stringFormatter_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/wissance/stringFormatter" 10 | ) 11 | 12 | const _separator = ", " 13 | 14 | func TestMapToString(t *testing.T) { 15 | for name, test := range map[string]struct { 16 | str string 17 | expectedParts []string 18 | }{ 19 | "semicolon sep": { 20 | str: stringFormatter.MapToString( 21 | map[string]any{ 22 | "connectTimeout": 1000, 23 | "useSsl": true, 24 | "login": "sa", 25 | "password": "sa", 26 | }, 27 | "{key} : {value}", 28 | _separator, 29 | ), 30 | expectedParts: []string{ 31 | "connectTimeout : 1000", 32 | "useSsl : true", 33 | "login : sa", 34 | "password : sa", 35 | }, 36 | }, 37 | "arrow sep": { 38 | str: stringFormatter.MapToString( 39 | map[int]any{ 40 | 1: "value 1", 41 | 2: "value 2", 42 | -5: "value -5", 43 | }, 44 | "{key} => {value}", 45 | _separator, 46 | ), 47 | expectedParts: []string{ 48 | "1 => value 1", 49 | "2 => value 2", 50 | "-5 => value -5", 51 | }, 52 | }, 53 | "only value": { 54 | str: stringFormatter.MapToString( 55 | map[uint64]any{ 56 | 1: "value 1", 57 | 2: "value 2", 58 | 5: "value 5", 59 | }, 60 | "{value}", 61 | _separator, 62 | ), 63 | expectedParts: []string{ 64 | "value 1", 65 | "value 2", 66 | "value 5", 67 | }, 68 | }, 69 | } { 70 | t.Run(name, func(t *testing.T) { 71 | actualParts := strings.Split(test.str, _separator) 72 | assert.ElementsMatch(t, test.expectedParts, actualParts) 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | # timeout for analysis, e.g. 30s, 5m, default is 1m 3 | timeout: 30m 4 | 5 | modules-download-mode: readonly 6 | 7 | go: '1.21' 8 | 9 | output: 10 | # colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions 11 | # default is "colored-line-number" 12 | formats: code-climate 13 | 14 | linters: 15 | enable-all: false 16 | disable: 17 | - exhaustruct 18 | - gofumpt 19 | - testpackage 20 | - depguard 21 | - tagliatelle 22 | - ireturn 23 | - varnamelen 24 | - wrapcheck 25 | 26 | linters-settings: 27 | stylecheck: 28 | # Select the Go version to target. The default is '1.13'. 29 | # https://staticcheck.io/docs/options#checks 30 | checks: [ "all", "-ST1000" ] 31 | funlen: 32 | lines: 100 33 | gci: 34 | sections: 35 | - standard 36 | - default 37 | - prefix(github.com/wissance/stringFormatter) 38 | gocyclo: 39 | min-complexity: 5 40 | varnamelen: 41 | ignore-names: 42 | - id 43 | ignore-decls: 44 | - ok bool 45 | wrapcheck: 46 | ignorePackageGlobs: 47 | - google.golang.org/grpc/status 48 | - github.com/pkg/errors 49 | - golang.org/x/sync/errgroup 50 | gosec: 51 | excludes: 52 | - G204 53 | 54 | issues: 55 | exclude-rules: 56 | - path: _test\.go 57 | linters: 58 | - containedctx 59 | - gocyclo 60 | - cyclop 61 | - funlen 62 | - goerr113 63 | - varnamelen 64 | - staticcheck 65 | - maintidx 66 | - lll 67 | - paralleltest 68 | - dupl 69 | - typecheck 70 | - wsl 71 | - govet 72 | - path: main\.go 73 | linters: 74 | - gochecknoglobals 75 | - lll 76 | - funlen 77 | version: 2 -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Go CI 2 | 3 | on: 4 | pull_request: 5 | branches: [develop, master] 6 | push: 7 | branches: [develop, master] 8 | 9 | jobs: 10 | build-linux: 11 | name: Build stringFormatter on linux 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4.1.7 15 | - name: Set up Go 16 | uses: actions/setup-go@v5.2.0 17 | with: 18 | go-version: '1.21' 19 | - name: Build 20 | run: go version && go build -v ./... 21 | 22 | build-windows: 23 | name: Build stringFormatter on windows 24 | runs-on: windows-latest 25 | steps: 26 | - uses: actions/checkout@v4.1.7 27 | - name: Set up Go 28 | uses: actions/setup-go@v5.2.0 29 | with: 30 | go-version: '1.21' 31 | - name: Build 32 | run: go version && go build -v ./... 33 | 34 | all-tests-linux: 35 | name: Run all tests on linux 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v4.1.7 39 | - name: Set up Go 40 | uses: actions/setup-go@v5.2.0 41 | with: 42 | go-version: '1.21' 43 | - name: Test all 44 | run: go version && go mod tidy && go test -v ./... 45 | 46 | #all-tests-windows: 47 | # name: Run all tests on windows 48 | # runs-on: windows-latest 49 | # steps: 50 | # - uses: actions/checkout@v4.1.7 51 | # - name: Set up Go 52 | # uses: actions/setup-go@v5.0.2 53 | # - name: Test all 54 | # run: go test -v ./... 55 | # on windows don't work linux containers by default 56 | 57 | lint: 58 | name: Run golangci linters 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v4.1.7 62 | - name: Set up Go 63 | uses: actions/setup-go@v5.2.0 64 | with: 65 | go-version: '1.21' 66 | - name: Run golangci-lint 67 | uses: golangci/golangci-lint-action@v6.1.0 68 | with: 69 | version: v1.63.4 70 | args: --timeout 3m --config .golangci.yaml -------------------------------------------------------------------------------- /slicetostring_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package stringFormatter_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/wissance/stringFormatter" 6 | "testing" 7 | ) 8 | 9 | func BenchmarkSliceToStringAdvancedWith8IntItems(b *testing.B) { 10 | slice := []any{100, 200, 300, 400, 500, 600, 700, 800} 11 | separator := "," 12 | for i := 0; i < b.N; i++ { 13 | _ = stringFormatter.SliceToString(&slice, &separator) 14 | } 15 | } 16 | 17 | func BenchmarkSliceStandard8IntItems(b *testing.B) { 18 | slice := []any{100, 200, 300, 400, 500, 600, 700, 800} 19 | for i := 0; i < b.N; i++ { 20 | _ = fmt.Sprintf("%+q", slice) 21 | } 22 | } 23 | 24 | func BenchmarkSliceToStringAdvanced10MixedItems(b *testing.B) { 25 | slice := []any{100, "200", 300, "400", 500, 600, "700", 800, 1.09, "hello"} 26 | separator := "," 27 | for i := 0; i < b.N; i++ { 28 | _ = stringFormatter.SliceToString(&slice, &separator) 29 | } 30 | } 31 | 32 | func BenchmarkSliceToStringAdvanced10TypedItems(b *testing.B) { 33 | slice := []int32{100, 102, 300, 404, 500, 600, 707, 800, 909, 1024} 34 | for i := 0; i < b.N; i++ { 35 | _ = stringFormatter.Format("{0:L,}", []any{slice}) 36 | } 37 | } 38 | 39 | func BenchmarkSliceStandard10MixedItems(b *testing.B) { 40 | slice := []any{100, "200", 300, "400", 500, 600, "700", 800, 1.09, "hello"} 41 | for i := 0; i < b.N; i++ { 42 | _ = fmt.Sprintf("%+q", slice) 43 | } 44 | } 45 | 46 | func BenchmarkSliceToStringAdvanced20StrItems(b *testing.B) { 47 | slice := []any{"str1", "str2", "str3", "str4", "str5", "str6", "str7", "str8", "str9", "str10", 48 | "str11", "str12", "str13", "str14", "str15", "str16", "str17", "str18", "str19", "str20"} 49 | for i := 0; i < b.N; i++ { 50 | _ = stringFormatter.Format("{0:L,}", []any{slice}) 51 | } 52 | } 53 | 54 | func BenchmarkSliceToStringAdvanced20TypedStrItems(b *testing.B) { 55 | slice := []string{"str1", "str2", "str3", "str4", "str5", "str6", "str7", "str8", "str9", "str10", 56 | "str11", "str12", "str13", "str14", "str15", "str16", "str17", "str18", "str19", "str20"} 57 | separator := "," 58 | for i := 0; i < b.N; i++ { 59 | _ = stringFormatter.SliceSameTypeToString(&slice, &separator) 60 | } 61 | } 62 | 63 | func BenchmarkSliceStandard20StrItems(b *testing.B) { 64 | slice := []any{"str1", "str2", "str3", "str4", "str5", "str6", "str7", "str8", "str9", "str10", 65 | "str11", "str12", "str13", "str14", "str15", "str16", "str17", "str18", "str19", "str20"} 66 | for i := 0; i < b.N; i++ { 67 | _ = fmt.Sprintf("%+q", slice) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /formatter_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package stringFormatter_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/wissance/stringFormatter" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func BenchmarkFormat4Arg(b *testing.B) { 11 | for i := 0; i < b.N; i++ { 12 | _ = stringFormatter.Format( 13 | "Today is : {0}, atmosphere pressure is : {1} mmHg, temperature: {2}, location: {3}", 14 | time.Now().String(), 725, -1.54, "Yekaterinburg", 15 | ) 16 | } 17 | } 18 | 19 | func BenchmarkFormat4ArgAdvanced(b *testing.B) { 20 | for i := 0; i < b.N; i++ { 21 | _ = stringFormatter.Format( 22 | "Today is : {0}, atmosphere pressure is : {1:E2} mmHg, temperature: {2:E3}, location: {3}", 23 | time.Now().String(), 725, -15.54, "Yekaterinburg", 24 | ) 25 | } 26 | } 27 | 28 | func BenchmarkFmt4Arg(b *testing.B) { 29 | for i := 0; i < b.N; i++ { 30 | _ = fmt.Sprintf( 31 | "Today is : %s, atmosphere pressure is : %d mmHg, temperature: %f, location: %s", 32 | time.Now().String(), 725, -1.54, "Yekaterinburg", 33 | ) 34 | } 35 | } 36 | 37 | func BenchmarkFmt4ArgAdvanced(b *testing.B) { 38 | for i := 0; i < b.N; i++ { 39 | _ = fmt.Sprintf( 40 | "Today is : %s, atmosphere pressure is : %.3e mmHg, temperature: %.2f, location: %s", 41 | time.Now().String(), 725.0, -15.54, "Yekaterinburg", 42 | ) 43 | } 44 | } 45 | 46 | func BenchmarkFormat6Arg(b *testing.B) { 47 | for i := 0; i < b.N; i++ { 48 | _ = stringFormatter.Format( 49 | "Today is : {0}, atmosphere pressure is : {1} mmHg, temperature: {2}, location: {3}, coord:{4}-{5}", 50 | time.Now().String(), 725, -1.54, "Yekaterinburg", "64.245", "37.895", 51 | ) 52 | } 53 | } 54 | 55 | func BenchmarkFmt6Arg(b *testing.B) { 56 | for i := 0; i < b.N; i++ { 57 | _ = fmt.Sprintf( 58 | "Today is : %s, atmosphere pressure is : %d mmHg, temperature: %f, location: %s, coords: %s-%s", 59 | time.Now().String(), 725, -1.54, "Yekaterinburg", "64.245", "37.895", 60 | ) 61 | } 62 | } 63 | 64 | func BenchmarkFormatComplex7Arg(b *testing.B) { 65 | args := map[string]any{ 66 | "temperature": -10, 67 | "location": "Yekaterinburg", 68 | "time": time.Now().String(), 69 | "pressure": 725, 70 | "humidity": 34, 71 | "longitude": "64.245", 72 | "latitude": "35.489", 73 | } 74 | for i := 0; i < b.N; i++ { 75 | _ = stringFormatter.FormatComplex( 76 | "Today is : {time}, atmosphere pressure is : {pressure} mmHg, humidity: {humidity}, temperature: {temperature}, location: {location}, coords:{longitude}-{latitude}", 77 | args, 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /stringstyle_formatter_test.go: -------------------------------------------------------------------------------- 1 | package stringFormatter_test 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/wissance/stringFormatter" 6 | "testing" 7 | ) 8 | 9 | func TestSetFormattingStyleWithoutCaseModification(t *testing.T) { 10 | for name, test := range map[string]struct { 11 | text string 12 | expected string 13 | newStyle stringFormatter.FormattingStyle 14 | }{ 15 | "snake-to-kebab-simple": { 16 | text: "my_super_func", 17 | expected: "my-super-func", 18 | newStyle: stringFormatter.Kebab, 19 | }, 20 | "kebab-to-snake-simple": { 21 | text: "my-super-func", 22 | expected: "my_super_func", 23 | newStyle: stringFormatter.Snake, 24 | }, 25 | "lower-case-camel-to-snake-simple": { 26 | text: "mySuperFunc", 27 | expected: "my_Super_Func", 28 | newStyle: stringFormatter.Snake, 29 | }, 30 | "snake-to-camel-simple": { 31 | text: "my_super_func", 32 | expected: "mySuperFunc", 33 | newStyle: stringFormatter.Camel, 34 | }, 35 | "camel-to-snake-with-underscore-the-end": { 36 | text: "myVal_", 37 | expected: "my_Val_", 38 | newStyle: stringFormatter.Snake, 39 | }, 40 | "mixed-to-camel-simple": { 41 | text: "TestGetManyMethod_WithDefaultParams", 42 | expected: "TestGetManyMethodWithDefaultParams", 43 | newStyle: stringFormatter.Camel, 44 | }, 45 | "no-changes-simple": { 46 | text: "_my_variable", 47 | expected: "_my_variable", 48 | newStyle: stringFormatter.Snake, 49 | }, 50 | "snake_to_camel_with_underscore_at_start": { 51 | text: "_my_variable", 52 | expected: "MyVariable", 53 | newStyle: stringFormatter.Camel, 54 | }, 55 | "camel-with_abbreviation-to-snake": { 56 | text: "convertToJSON", 57 | expected: "convert_To_JSON", 58 | newStyle: stringFormatter.Snake, 59 | }, 60 | } { 61 | t.Run(name, func(t *testing.T) { 62 | actual := stringFormatter.SetStyle(&test.text, test.newStyle, stringFormatter.NoChanges, 63 | stringFormatter.NoChanges) 64 | assert.Equal(t, test.expected, actual) 65 | }) 66 | } 67 | } 68 | 69 | func TestSetFormattingStyleWithCaseModification(t *testing.T) { 70 | for name, test := range map[string]struct { 71 | text string 72 | expected string 73 | newStyle stringFormatter.FormattingStyle 74 | firstSymbolCase stringFormatter.CaseSetting 75 | textCase stringFormatter.CaseSetting 76 | }{ 77 | "snake-to-kebab-upper-case": { 78 | text: "my_super_func", 79 | expected: "MY-SUPER-FUNC", 80 | newStyle: stringFormatter.Kebab, 81 | firstSymbolCase: stringFormatter.ToUpper, 82 | textCase: stringFormatter.ToUpper, 83 | }, 84 | "snake-to-camel-starting-from-lower-case": { 85 | text: "my_super_func", 86 | expected: "mySuperFunc", 87 | newStyle: stringFormatter.Camel, 88 | firstSymbolCase: stringFormatter.ToLower, 89 | textCase: stringFormatter.NoChanges, 90 | }, 91 | "camel-to-upper-case-snake": { 92 | text: "mySuperFunc", 93 | expected: "MY_SUPER_FUNC", 94 | newStyle: stringFormatter.Snake, 95 | firstSymbolCase: stringFormatter.ToUpper, 96 | textCase: stringFormatter.ToUpper, 97 | }, 98 | } { 99 | t.Run(name, func(t *testing.T) { 100 | actual := stringFormatter.SetStyle(&test.text, test.newStyle, test.firstSymbolCase, 101 | test.textCase) 102 | assert.Equal(t, test.expected, actual) 103 | }) 104 | } 105 | } 106 | 107 | func TestGetFormattingStyleOptions(t *testing.T) { 108 | for name, test := range map[string]struct { 109 | style string 110 | expectedStyle stringFormatter.FormattingStyle 111 | expectedFirstSymbolCase stringFormatter.CaseSetting 112 | expectedTextCase stringFormatter.CaseSetting 113 | }{ 114 | "snake-lower-case-style": { 115 | style: "snake", 116 | expectedStyle: stringFormatter.Snake, 117 | expectedFirstSymbolCase: stringFormatter.ToLower, 118 | expectedTextCase: stringFormatter.ToLower, 119 | }, 120 | "snake-upper-case-style": { 121 | style: "SNAKE", 122 | expectedStyle: stringFormatter.Snake, 123 | expectedFirstSymbolCase: stringFormatter.ToUpper, 124 | expectedTextCase: stringFormatter.ToUpper, 125 | }, 126 | "kebab-lower-case-style": { 127 | style: "kebab", 128 | expectedStyle: stringFormatter.Kebab, 129 | expectedFirstSymbolCase: stringFormatter.ToLower, 130 | expectedTextCase: stringFormatter.ToLower, 131 | }, 132 | "kebab-upper-case-style": { 133 | style: "KEBAB", 134 | expectedStyle: stringFormatter.Kebab, 135 | expectedFirstSymbolCase: stringFormatter.ToUpper, 136 | expectedTextCase: stringFormatter.ToUpper, 137 | }, 138 | "camel-lower-case-style": { 139 | style: "camel", 140 | expectedStyle: stringFormatter.Camel, 141 | expectedFirstSymbolCase: stringFormatter.ToLower, 142 | expectedTextCase: stringFormatter.NoChanges, 143 | }, 144 | "camel-upper-case-style": { 145 | style: "Camel", 146 | expectedStyle: stringFormatter.Camel, 147 | expectedFirstSymbolCase: stringFormatter.ToUpper, 148 | expectedTextCase: stringFormatter.NoChanges, 149 | }, 150 | } { 151 | t.Run(name, func(t *testing.T) { 152 | actualStyle, actualFirstSymbolCase, actualTextCase := stringFormatter.GetFormattingStyleOptions(test.style) 153 | assert.Equal(t, test.expectedStyle, actualStyle) 154 | assert.Equal(t, test.expectedFirstSymbolCase, actualFirstSymbolCase) 155 | assert.Equal(t, test.expectedTextCase, actualTextCase) 156 | }) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /stringstyle_formatter.go: -------------------------------------------------------------------------------- 1 | package stringFormatter 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | ) 7 | 8 | type FormattingStyle string 9 | type CaseSetting int 10 | 11 | const ( 12 | Camel FormattingStyle = "camel" 13 | Snake FormattingStyle = "snake" 14 | Kebab FormattingStyle = "kebab" 15 | ) 16 | 17 | const ( 18 | ToUpper CaseSetting = 1 19 | ToLower CaseSetting = 2 20 | NoChanges CaseSetting = 3 21 | ) 22 | 23 | type styleInc struct { 24 | Index int 25 | Style FormattingStyle 26 | } 27 | 28 | var styleSigns = map[rune]FormattingStyle{ 29 | '_': Snake, 30 | '-': Kebab, 31 | } 32 | 33 | // SetStyle is a function that converts text with code to defined code style. 34 | /* Set text like a code style to on from FormattingStyle (Camel, Snake, or Kebab) 35 | * conversion of abbreviations like JSON, USB, and so on is going like a regular text 36 | * for current version, therefore they these abbreviations could be in a different 37 | * case after conversion. 38 | * Case settings apply in the following order : 1 - textCase, 2 - firstSymbol. 39 | * If you are not applying textCase to text converting from Camel to Snake or Kebab 40 | * result is lower case styled text. textCase does not apply to Camel style. 41 | * Parameters: 42 | * - text - pointer to text 43 | * - style - new code style 44 | * - firstSymbol - case settings for first symbol 45 | * - textCase - case settings for whole text except first symbol 46 | * Returns : new string with formatted line 47 | */ 48 | func SetStyle(text *string, style FormattingStyle, firstSymbol CaseSetting, textCase CaseSetting) string { 49 | if text == nil { 50 | return "" 51 | } 52 | sb := strings.Builder{} 53 | sb.Grow(len(*text)) 54 | stats := defineFormattingStyle(text) 55 | startIndex := 0 56 | endIndex := 0 57 | // todo UMV think how to process .... 58 | // we could have many stats at the same time, probably we should use some config in the future 59 | // iterate over the map 60 | for _, v := range stats { 61 | endIndex = v.Index 62 | if endIndex < startIndex { 63 | continue 64 | } 65 | sb.WriteString((*text)[startIndex:endIndex]) 66 | startIndex = v.Index 67 | 68 | switch style { 69 | case Kebab: 70 | sb.WriteString("-") 71 | case Snake: 72 | sb.WriteString("_") 73 | case Camel: 74 | // in case of convert to Camel we should skip v.Index (because it is _ or -) 75 | if v.Style == Camel { 76 | sb.WriteRune(unicode.ToUpper(rune((*text)[endIndex]))) 77 | } else { 78 | sb.WriteRune(unicode.ToUpper(rune((*text)[endIndex+1]))) 79 | } 80 | startIndex += 1 81 | } 82 | if v.Style != Camel { 83 | startIndex += 1 84 | } 85 | } 86 | sb.WriteString((*text)[startIndex:]) 87 | result := strings.Builder{} 88 | if style != Camel { 89 | switch textCase { 90 | case ToUpper: 91 | result.WriteString(strings.ToUpper(sb.String()[1:])) 92 | case ToLower: 93 | result.WriteString(strings.ToLower(sb.String()[1:])) 94 | case NoChanges: 95 | result.WriteString(sb.String()[1:]) 96 | } 97 | } else { 98 | result.WriteString(sb.String()[1:]) 99 | } 100 | 101 | switch firstSymbol { 102 | case ToUpper: 103 | return strings.ToUpper(sb.String()[:1]) + result.String() 104 | case ToLower: 105 | return strings.ToLower(sb.String()[:1]) + result.String() 106 | case NoChanges: 107 | return sb.String()[:1] + result.String() 108 | default: 109 | return sb.String()[:1] + result.String() 110 | } 111 | } 112 | 113 | // GetFormattingStyleOptions function that defines formatting style, case of first char and result from string 114 | /* 115 | * 116 | */ 117 | func GetFormattingStyleOptions(style string) (FormattingStyle, CaseSetting, CaseSetting) { 118 | styleLower := strings.ToLower(style) 119 | var formattingStyle FormattingStyle 120 | firstSymbolCase := ToLower 121 | textCase := NoChanges 122 | switch styleLower { 123 | case string(Camel): 124 | formattingStyle = Camel 125 | case string(Snake): 126 | formattingStyle = Snake 127 | case string(Kebab): 128 | formattingStyle = Kebab 129 | } 130 | 131 | runes := []rune(style) 132 | firstSymbolIsUpper := isSymbolIsUpper(runes[0]) 133 | if firstSymbolIsUpper { 134 | firstSymbolCase = ToUpper 135 | } 136 | 137 | if formattingStyle != Camel { 138 | allSymbolsUpperCase := isStringIsUpper(runes) 139 | if allSymbolsUpperCase { 140 | textCase = ToUpper 141 | } else { 142 | textCase = ToLower 143 | } 144 | } 145 | 146 | return formattingStyle, firstSymbolCase, textCase 147 | } 148 | 149 | // defineFormattingStyle 150 | /* This function defines what formatting style is using in text 151 | * If there are no transitions between symbols then here we have NoFormatting style 152 | * Didn't decide yet what to do if we are having multiple signatures 153 | * i.e. multiple_signs-at-sameTime . 154 | * Parameters: 155 | * - text - a sequence of symbols to check 156 | * Returns: formatting style using in the text 157 | */ 158 | func defineFormattingStyle(text *string) []styleInc { 159 | // symbol analyze, for camel case pattern -> aA, for kebab -> a-a, for snake -> a_a 160 | inclusions := make([]styleInc, 0) 161 | runes := []rune(*text) 162 | for pos, char := range runes { 163 | // define style and add stats 164 | style, ok := styleSigns[char] 165 | if !ok { 166 | // 1. Probably current symbol is not a sign and we should continue 167 | if pos > 0 && pos < len(runes)-1 { 168 | charIsUpperCase := isSymbolIsUpper(char) 169 | prevChar := runes[pos-1] 170 | prevCharIsUpperCase := unicode.IsUpper(prevChar) 171 | if charIsUpperCase && !prevCharIsUpperCase { 172 | style = Camel 173 | } 174 | } 175 | } 176 | if style != "" { 177 | inclusions = append(inclusions, styleInc{Index: pos, Style: style}) 178 | } 179 | } 180 | return inclusions 181 | } 182 | 183 | func isSymbolIsUpper(symbol rune) bool { 184 | return unicode.IsUpper(symbol) && unicode.IsLetter(symbol) 185 | } 186 | 187 | func isStringIsUpper(str []rune) bool { 188 | isUpper := true 189 | for _, r := range str { 190 | if unicode.IsLetter(r) { 191 | isUpper = isUpper && unicode.IsUpper(r) 192 | } 193 | } 194 | return isUpper 195 | } 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StringFormatter 2 | 3 | * A set of a ***high performance string tools*** that helps to build strings from templates and process text faster than with `fmt`!!!. 4 | * Allows to **format code in appropriate style** (`Snake`, `Kebab`, `Camel`) and case. 5 | * Slice printing is **50% faster with 8 items** slice and **250% with 20 items** slice 6 | 7 | ![GitHub go.mod Go version (subdirectory of monorepo)](https://img.shields.io/github/go-mod/go-version/wissance/stringFormatter?style=plastic) 8 | ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/wissance/stringFormatter?style=plastic) 9 | ![GitHub issues](https://img.shields.io/github/issues/wissance/stringFormatter?style=plastic) 10 | ![GitHub Release Date](https://img.shields.io/github/release-date/wissance/stringFormatter) 11 | ![GitHub release (latest by date)](https://img.shields.io/github/downloads/wissance/stringFormatter/v1.6.1/total?style=plastic) 12 | 13 | ![String Formatter: a convenient string formatting tool](img/sf_cover.png) 14 | 15 | ## 1. Features 16 | 17 | 1. Text formatting with template using traditional for `C#, Python programmers style` - `{0}`, `{name}` that faster then `fmt` does: 18 | ![String Formatter: a convenient string formatting tool](img/benchmarks_adv.png) 19 | 2. Additional text utilities: 20 | - convert ***map to string*** using one of predefined formats (see `text_utils.go`) 21 | 3. Code Style formatting utilities 22 | - convert `snake`/`kebab`/`camel` programming code to each other and vice versa (see `stringstyle_formatter.go`). 23 | 4. `StringFormatter` aka `sf` **is safe** (`SAST` and tests were running automatically on push) 24 | 25 | ### 1. Text formatting from templates 26 | 27 | #### 1.1 Description 28 | 29 | This is a GO module for ***template text formatting in syntax like in C# or/and Python*** using: 30 | - `{n}` , n here is a number to notes order of argument list to use i.e. `{0}`, `{1}` 31 | - `{name}` to notes arguments by name i.e. `{name}`, `{last_name}`, `{address}` and so on ... 32 | 33 | #### 1.2 Examples 34 | 35 | ##### 1.2.1 Format by arg order 36 | 37 | i.e. you have following template: `"Hello {0}, we are greeting you here: {1}!"` 38 | 39 | if you call Format with args "manager" and "salesApp" : 40 | 41 | ```go 42 | formattedStr := stringFormatter.Format("Hello {0}, we are greeting you here: {1}!", "manager", "salesApp") 43 | ``` 44 | 45 | you get string `"Hello manager, we are greeting you here: salesApp!"` 46 | 47 | ##### 1.2.2 Format by arg key 48 | 49 | i.e. you have following template: `"Hello {user} what are you doing here {app} ?"` 50 | 51 | if you call `FormatComplex` with args `"vpupkin"` and `"mn_console"` `FormatComplex("Hello {user} what are you doing here {app} ?", map[string]any{"user":"vpupkin", "app":"mn_console"})` 52 | 53 | you get string `"Hello vpupkin what are you doing here mn_console ?"` 54 | 55 | another example is: 56 | 57 | ```go 58 | strFormatResult = stringFormatter.FormatComplex( 59 | "Current app settings are: ipAddr: {ipaddr}, port: {port}, use ssl: {ssl}.", 60 | map[string]any{"ipaddr":"127.0.0.1", "port":5432, "ssl":false}, 61 | ) 62 | ``` 63 | a result will be: `"Current app settings are: ipAddr: 127.0.0.1, port: 5432, use ssl: false."`` 64 | 65 | ##### 1.2.3 Advanced arguments formatting 66 | 67 | For more convenient lines formatting we should choose how arguments are representing in output text, 68 | `stringFormatter` supports following format options: 69 | 1. Bin number formatting 70 | - `{0:B}, 15 outputs -> 1111` 71 | - `{0:B8}, 15 outputs -> 00001111` 72 | 2. Hex number formatting 73 | - `{0:X}, 250 outputs -> fa` 74 | - `{0:X4}, 250 outputs -> 00fa` 75 | 3. Oct number formatting 76 | - `{0:o}, 11 outputs -> 14` 77 | 4. Float point number formatting 78 | - `{0:E2}, 191.0478 outputs -> 1.91e+02` 79 | - `{0:F}, 10.4567890 outputs -> 10.456789` 80 | - `{0:F4}, 10.4567890 outputs -> 10.4568` 81 | - `{0:F8}, 10.4567890 outputs -> 10.45678900` 82 | 5. Percentage output 83 | - `{0:P100}, 12 outputs -> 12%` 84 | 6. Lists 85 | - `{0:L-}, [1,2,3] outputs -> 1-2-3` 86 | - `{0:L, }, [1,2,3] outputs -> 1, 2, 3` 87 | 7. Code 88 | - `{0:c:snake}, myFunc outputs -> my_func` 89 | - `{0:c:Snake}, myFunc outputs -> My_func` 90 | - `{0:c:SNAKE}, read-timeout outputs -> READ_TIMEOUT` 91 | - `{0:c:camel}, my_variable outputs -> myVariable` 92 | - `{0:c:Camel}, my_variable outputs -> MyVariable` 93 | 94 | ##### 1.2.4 Benchmarks of the Format and FormatComplex functions 95 | 96 | benchmark could be running using following commands from command line: 97 | * to see `Format` result - `go test -bench=Format -benchmem -cpu 1` 98 | * to see `fmt` result - `go test -bench=Fmt -benchmem -cpu 1` 99 | 100 | ### 2. Text utilities 101 | 102 | #### 2.1 Map to string utility 103 | 104 | `MapToString` function allows to convert map with primitive key to string using format, including key and value, e.g.: 105 | * `{key} => {value}` 106 | * `{key} : {value}` 107 | * `{value}` 108 | 109 | For example: 110 | ```go 111 | options := map[string]any{ 112 | "connectTimeout": 1000, 113 | "useSsl": true, 114 | "login": "sa", 115 | "password": "sa", 116 | } 117 | 118 | str := stringFormatter.MapToString(&options, "{key} : {value}", ", ") 119 | // NOTE: order of key-value pairs is not guranteed though 120 | // str will be something like: 121 | "connectTimeout : 1000, useSsl : true, login : sa, password : sa" 122 | ``` 123 | 124 | #### 2.2 Benchmarks of the MapToString function 125 | 126 | * to see `MapToStr` result - `go test -bench=MapToStr -benchmem -cpu 1` 127 | 128 | ![MapToStr benchmarks](/img/map2str_benchmarks.png) 129 | 130 | #### 2.3 Slice to string utility 131 | 132 | `SliceToString` - function that converts slice with passed separation between items to string. 133 | ```go 134 | slice := []any{100, "200", 300, "400", 500, 600, "700", 800, 1.09, "hello"} 135 | separator := "," 136 | result := stringFormatter.SliceToString(&slice, &separator) 137 | ``` 138 | 139 | `SliceSameTypeToString` - function that converts typed slice to line with separator 140 | ```go 141 | separator := ":" 142 | numericSlice := []int{100, 200, 400, 800} 143 | result := stringFormatter.SliceSameTypeToString(&numericSlice, &separator) 144 | ``` 145 | 146 | #### 2.4 Benchmarks of the SliceToString function 147 | 148 | `sf` is rather fast then `fmt` 2.5 times (250%) faster on slice with 20 items, see benchmark: 149 | ![SliceToStr benchmarks](/img/slice2str_benchmarks.png) 150 | 151 | ### 3. Contributors 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /formatter_test.go: -------------------------------------------------------------------------------- 1 | package stringFormatter_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/wissance/stringFormatter" 10 | ) 11 | 12 | const _address = "grpcs://127.0.0.1" 13 | 14 | type meteoData struct { 15 | Int int 16 | Str string 17 | Double float64 18 | Err error 19 | } 20 | 21 | func TestFormat(t *testing.T) { 22 | for name, test := range map[string]struct { 23 | template string 24 | args []any 25 | expected string 26 | }{ 27 | "all args in place": { 28 | template: "Hello i am {0}, my age is {1} and i am waiting for {2}, because i am {0}!", 29 | args: []any{"Michael Ushakov (Evillord666)", "34", `"Great Success"`}, 30 | expected: `Hello i am Michael Ushakov (Evillord666), my age is 34 and i am waiting for "Great Success", because i am Michael Ushakov (Evillord666)!`, 31 | }, 32 | "too large index": { 33 | template: "We are wondering if these values would be replaced : {5}, {4}, {0}", 34 | args: []any{"one", "two", "three"}, 35 | expected: "We are wondering if these values would be replaced : {5}, {4}, one", 36 | }, 37 | "no args": { 38 | template: "No args ... : {0}, {1}, {2}", 39 | args: nil, 40 | expected: "No args ... : {0}, {1}, {2}", 41 | }, 42 | "format json": { 43 | template: ` 44 | { 45 | "Comment": "Call Lambda with GRPC", 46 | "StartAt": "CallLambdaWithGrpc", 47 | "States": {"CallLambdaWithGrpc": {"Type": "Task", "Resource": "{0}:get ad user", "End": true}} 48 | }`, 49 | args: []any{_address}, 50 | expected: ` 51 | { 52 | "Comment": "Call Lambda with GRPC", 53 | "StartAt": "CallLambdaWithGrpc", 54 | "States": {"CallLambdaWithGrpc": {"Type": "Task", "Resource": "grpcs://127.0.0.1:get ad user", "End": true}} 55 | }`, 56 | }, 57 | "multiple nested curly brackets": { 58 | template: `{"StartAt": "S0", "States": {"S0": {"Type": "Map" , ` + 59 | `"Iterator": {"StartAt": "SI0", "States": {"SI0": {"Type": "Pass", "End": true}}}` + 60 | `, "End": true}}}`, 61 | args: []any{""}, 62 | expected: `{"StartAt": "S0", "States": {"S0": {"Type": "Map" , "Iterator": {"StartAt": "SI0", "States": {"SI0": {"Type": "Pass", "End": true}}}, "End": true}}}`, 63 | }, 64 | "indexes out of args range": { 65 | template: "{3} - rings to the immortal elfs, {7} to dwarfs, {9} to greedy people and {1} to control everything", 66 | args: []any{"0", "1", "2", "3"}, 67 | expected: "3 - rings to the immortal elfs, {7} to dwarfs, {9} to greedy people and 1 to control everything", 68 | }, 69 | "format integers": { 70 | template: `Here we are testing integers "int8": {0}, "int16": {1}, "int32": {2}, "int64": {3} and finally "int": {4}`, 71 | args: []any{int8(8), int16(-16), int32(32), int64(-64), int(123)}, 72 | expected: `Here we are testing integers "int8": 8, "int16": -16, "int32": 32, "int64": -64 and finally "int": 123`, 73 | }, 74 | "format unsigneds": { 75 | template: `Here we are testing integers "uint8": {0}, "uint16": {1}, "uint32": {2}, "uint64": {3} and finally "uint": {4}`, 76 | args: []any{uint8(8), uint16(16), uint32(32), uint64(64), uint(128)}, 77 | expected: `Here we are testing integers "uint8": 8, "uint16": 16, "uint32": 32, "uint64": 64 and finally "uint": 128`, 78 | }, 79 | "format floats": { 80 | template: `Here we are testing floats "float32": {0}, "float64":{1}`, 81 | args: []any{float32(1.24), float64(1.56)}, 82 | expected: `Here we are testing floats "float32": 1.24, "float64":1.56`, 83 | }, 84 | "format bools": { 85 | template: `Here we are testing "bool" args: {0}, {1}`, 86 | args: []any{false, true}, 87 | expected: `Here we are testing "bool" args: false, true`, 88 | }, 89 | "format complex": { 90 | template: `Here we are testing "complex64" {0} and "complex128": {1}`, 91 | args: []any{complex64(complex(1.0, 6.0)), complex(2.3, 3.2)}, 92 | expected: `Here we are testing "complex64" (1+6i) and "complex128": (2.3+3.2i)`, 93 | }, 94 | "doubly curly brackets": { 95 | template: "Hello i am {{0}}, my age is {1} and i am waiting for {2}, because i am {0}!", 96 | args: []any{"Michael Ushakov (Evillord666)", "34", `"Great Success"`}, 97 | expected: `Hello i am {0}, my age is 34 and i am waiting for "Great Success", because i am Michael Ushakov (Evillord666)!`, 98 | }, 99 | "doubly curly brackets at the end": { 100 | template: "At the end {{0}}", 101 | args: []any{"s"}, 102 | expected: "At the end {0}", 103 | }, 104 | "quadro curly brackets in the middle": { 105 | template: "Not at the end {{{{0}}}}, in the middle", 106 | args: []any{"s"}, 107 | expected: "Not at the end {{0}}, in the middle", 108 | }, 109 | "struct arg": { 110 | template: "Example is: {0}", 111 | args: []any{ 112 | meteoData{ 113 | Int: 123, 114 | Str: "This is a test str, nothing more special", 115 | Double: -1.098743, 116 | Err: errors.New("main question error, is 42"), 117 | }, 118 | }, 119 | expected: "Example is: {123 This is a test str, nothing more special -1.098743 main question error, is 42}", 120 | }, 121 | "open bracket at the end of line of go line": { 122 | template: "type serviceHealth struct {", 123 | args: []any{}, 124 | expected: "type serviceHealth struct {", 125 | }, 126 | "open bracket at the end of line of go line with {} inside": { 127 | template: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) {", 128 | args: []any{}, 129 | expected: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) {", 130 | }, 131 | 132 | "close bracket at the end of line of go line with {} inside": { 133 | template: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) }", 134 | args: []any{}, 135 | expected: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) }", 136 | }, 137 | 138 | "no bracket at the end of line with {} inside": { 139 | template: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) ", 140 | args: []any{}, 141 | expected: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) ", 142 | }, 143 | "open bracket at the end of line of go line with multiple {} inside": { 144 | template: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}, additionalData interface{}) {", 145 | args: []any{}, 146 | expected: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}, additionalData interface{}) {", 147 | }, 148 | "commentaries after bracket": { 149 | template: "switch app.appConfig.ServerCfg.Schema { //nolint:exhaustive", 150 | args: []any{}, 151 | expected: "switch app.appConfig.ServerCfg.Schema { //nolint:exhaustive", 152 | }, 153 | "bracket in the middle": { 154 | template: "in the middle - { at the end - nothing", 155 | args: []any{}, 156 | expected: "in the middle - { at the end - nothing", 157 | }, 158 | "code line with interface": { 159 | template: "[]any{singleValue}", 160 | args: []any{}, 161 | expected: "[]any{singleValue}", 162 | }, 163 | "code line with interface with val": { 164 | template: "[]any{{{0}}}", 165 | args: []any{"\"USSR!\""}, 166 | expected: "[]any{\"USSR!\"}", 167 | }, 168 | "2-symb str": { 169 | template: "a}", 170 | args: []any{}, 171 | expected: "a}", 172 | }, 173 | "one symb segment": { 174 | template: "{x}", 175 | args: []any{}, 176 | expected: "{x}", 177 | }, 178 | "one symb template": { 179 | template: "{", 180 | args: []any{}, 181 | expected: "{", 182 | }, 183 | "one symb template2": { 184 | template: "}", 185 | args: []any{}, 186 | expected: "}", 187 | }, 188 | } { 189 | t.Run(name, func(t *testing.T) { 190 | assert.Equal(t, test.expected, stringFormatter.Format(test.template, test.args...)) 191 | }) 192 | } 193 | } 194 | 195 | func TestFormatWithArgFormatting(t *testing.T) { 196 | for name, test := range map[string]struct { 197 | template string 198 | args []any 199 | expected string 200 | }{ 201 | "numeric_test_1": { 202 | template: "This is the text with an only number formatting: decimal - {0} / {0 : D6}, scientific - {1} / {1 : e2}", 203 | args: []any{123, 191.0784}, 204 | expected: "This is the text with an only number formatting: decimal - 123 / 000123, scientific - 191.0784 / 1.91e+02", 205 | }, 206 | "numeric_test_2": { 207 | template: "This is the text with an only number formatting: binary - {0:B} / {0 : B8}, hexadecimal - {1:X} / {1 : X4}", 208 | args: []any{15, 250}, 209 | expected: "This is the text with an only number formatting: binary - 1111 / 00001111, hexadecimal - fa / 00fa", 210 | }, 211 | "numeric_test_3": { 212 | template: "This is the text with an only number formatting: decimal - {0:F} / {0 : F4} / {0:F8}", 213 | args: []any{10.5467890}, 214 | expected: "This is the text with an only number formatting: decimal - 10.546789 / 10.5468 / 10.54678900", 215 | }, 216 | "numeric_test_4": { 217 | template: "This is the text with percentage format - {0:P100} / {0 : P100.5}, and non normal percentage {1:P100}", 218 | args: []any{12, "ass"}, 219 | expected: "This is the text with percentage format - 12.00 / 11.94, and non normal percentage 0.00", 220 | }, 221 | "list_with_default_sep": { 222 | template: "This is a list(slice) test: {0:L}", 223 | args: []any{[]any{"s1", "s2", "s3"}}, 224 | expected: "This is a list(slice) test: s1,s2,s3", 225 | }, 226 | "list_with_dash_sep": { 227 | template: "This is a list(slice) test: {0:L-}", 228 | args: []any{[]any{"s1", "s2", "s3"}}, 229 | expected: "This is a list(slice) test: s1-s2-s3", 230 | }, 231 | "list_with_space_sep": { 232 | template: "This is a list(slice) test: {0:L }", 233 | args: []any{[]any{"s1", "s2", "s3"}}, 234 | expected: "This is a list(slice) test: s1 s2 s3", 235 | }, 236 | "docs_with_func_to_snake": { 237 | template: "This docs contains description of a \"{0:c:snake}\" function", 238 | args: []any{[]any{"callSoapService"}}, 239 | expected: "This docs contains description of a \"call_soap_service\" function", 240 | }, 241 | "docs_with_func_to_upper_case_snake": { 242 | template: "This docs contains depends on a \"{0:c:SNAKE}\" constant", 243 | args: []any{[]any{"ReadTimeout"}}, 244 | expected: "This docs contains depends on a \"READ_TIMEOUT\" constant", 245 | }, 246 | "notes-about-kebab": { 247 | template: "Nowadays we've got a very strange style it looks in a following manner \"{0:C:kebab}\" and called \"kebab\"", 248 | args: []any{[]any{"veryVeryStrange"}}, 249 | expected: "Nowadays we've got a very strange style it looks in a following manner \"very-very-strange\" and called \"kebab\"", 250 | }, 251 | } { 252 | // Run test here 253 | t.Run(name, func(t *testing.T) { 254 | // assert.NotNil(t, test) 255 | assert.Equal(t, test.expected, stringFormatter.Format(test.template, test.args...)) 256 | }) 257 | } 258 | } 259 | 260 | func TestFormatWithArgFormattingForTypedSlice(t *testing.T) { 261 | for name, test := range map[string]struct { 262 | template string 263 | args []any 264 | expected string 265 | }{ 266 | "list_with_int_slice": { 267 | template: "This is a list(slice) test: {0:L-}", 268 | args: []any{[]int{101, 202, 303}}, 269 | expected: "This is a list(slice) test: 101-202-303", 270 | }, 271 | "list_with_uint_slice": { 272 | template: "This is a list(slice) test: {0:L-}", 273 | args: []any{[]uint{102, 204, 308}}, 274 | expected: "This is a list(slice) test: 102-204-308", 275 | }, 276 | "list_with_int32_slice": { 277 | template: "This is a list(slice) test: {0:L-}", 278 | args: []any{[]int32{100, 200, 300}}, 279 | expected: "This is a list(slice) test: 100-200-300", 280 | }, 281 | "list_with_int64_slice": { 282 | template: "This is a list(slice) test: {0:L-}", 283 | args: []any{[]int64{1001, 2002, 3003}}, 284 | expected: "This is a list(slice) test: 1001-2002-3003", 285 | }, 286 | "list_with_float64_slice": { 287 | template: "This is a list(slice) test: {0:L-}", 288 | args: []any{[]float64{1.01, 2.02, 3.03}}, 289 | expected: "This is a list(slice) test: 1.01-2.02-3.03", 290 | }, 291 | "list_with_float32_slice": { 292 | template: "This is a list(slice) test: {0:L-}", 293 | args: []any{[]float32{5.01, 6.02, 7.03}}, 294 | expected: "This is a list(slice) test: 5.01-6.02-7.03", 295 | }, 296 | "list_with_bool_slice": { 297 | template: "This is a list(slice) test: {0:L-}", 298 | args: []any{[]bool{true, true, false}}, 299 | expected: "This is a list(slice) test: true-true-false", 300 | }, 301 | "list_with_string_slice": { 302 | template: "This is a list(slice) test: {0:L-}", 303 | args: []any{[]string{"s1", "s2", "s3"}}, 304 | expected: "This is a list(slice) test: s1-s2-s3", 305 | }, 306 | } { 307 | // Run test here 308 | t.Run(name, func(t *testing.T) { 309 | // assert.NotNil(t, test) 310 | assert.Equal(t, test.expected, stringFormatter.Format(test.template, test.args...)) 311 | }) 312 | } 313 | } 314 | 315 | // TestStrFormatWithComplicatedText - this test represents issue with complicated text 316 | func TestFormatComplex(t *testing.T) { 317 | for name, test := range map[string]struct { 318 | template string 319 | args map[string]any 320 | expected string 321 | }{ 322 | "numeric_test_1": { 323 | template: ` 324 | { 325 | "Comment": "Call Lambda with GRPC", 326 | "StartAt": "CallLambdaWithGrpc", 327 | "States": {"CallLambdaWithGrpc": {"Type": "Task", "Resource": "{address}:get ad user", "End": true}} 328 | }`, 329 | args: map[string]any{"address": _address}, 330 | expected: ` 331 | { 332 | "Comment": "Call Lambda with GRPC", 333 | "StartAt": "CallLambdaWithGrpc", 334 | "States": {"CallLambdaWithGrpc": {"Type": "Task", "Resource": "grpcs://127.0.0.1:get ad user", "End": true}} 335 | }`, 336 | }, 337 | "key not found": { 338 | template: "Hello: {username}, you earn {amount} $", 339 | args: map[string]any{"amount": 1000}, 340 | expected: "Hello: {username}, you earn 1000 $", 341 | }, 342 | "dialog": { 343 | template: "Hello {user} what are you doing here {app} ?", 344 | args: map[string]any{"user": "vpupkin", "app": "mn_console"}, 345 | expected: "Hello vpupkin what are you doing here mn_console ?", 346 | }, 347 | "info message": { 348 | template: "Current app settings are: ipAddr: {ipaddr}, port: {port}, use ssl: {ssl}.", 349 | args: map[string]any{"ipaddr": "127.0.0.1", "port": 5432, "ssl": false}, 350 | expected: "Current app settings are: ipAddr: 127.0.0.1, port: 5432, use ssl: false.", 351 | }, 352 | "one json line with open bracket at the end": { 353 | template: " \"server\": {", 354 | args: map[string]any{}, 355 | expected: " \"server\": {", 356 | }, 357 | "open bracket at the end of line of go line with {} inside": { 358 | template: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) {", 359 | args: map[string]any{}, 360 | expected: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) {", 361 | }, 362 | 363 | "open bracket at the end of line of go line with multiple {} inside": { 364 | template: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}, additionalData interface{}) {", 365 | args: map[string]any{}, 366 | expected: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}, additionalData interface{}) {", 367 | }, 368 | 369 | "close bracket at the end of line of go line with {} inside": { 370 | template: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) }", 371 | args: map[string]any{}, 372 | expected: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) }", 373 | }, 374 | "commentaries after bracket": { 375 | template: "switch app.appConfig.ServerCfg.Schema { //nolint:exhaustive", 376 | args: map[string]any{}, 377 | expected: "switch app.appConfig.ServerCfg.Schema { //nolint:exhaustive", 378 | }, 379 | "code line with interface": { 380 | template: "[]any{singleValue}", 381 | args: map[string]any{}, 382 | expected: "[]any{singleValue}", 383 | }, 384 | "code line with interface with val": { 385 | template: "[]any{{{val}}}", 386 | args: map[string]any{"val": "\"USSR!\""}, 387 | expected: "[]any{\"USSR!\"}", 388 | }, 389 | "2-symb str": { 390 | template: "a}", 391 | args: map[string]any{}, 392 | expected: "a}", 393 | }, 394 | "one symb segment": { 395 | template: "{x}", 396 | args: map[string]any{}, 397 | expected: "{x}", 398 | }, 399 | "one symb template": { 400 | template: "{", 401 | args: map[string]any{}, 402 | expected: "{", 403 | }, 404 | "one symb template2": { 405 | template: "}", 406 | args: map[string]any{}, 407 | expected: "}", 408 | }, 409 | } { 410 | t.Run(name, func(t *testing.T) { 411 | assert.Equal(t, test.expected, stringFormatter.FormatComplex(test.template, test.args)) 412 | }) 413 | } 414 | } 415 | 416 | func TestFormatComplexWithArgFormatting(t *testing.T) { 417 | for name, test := range map[string]struct { 418 | template string 419 | args map[string]any 420 | expected string 421 | }{ 422 | "numeric_test_1": { 423 | template: "This is the text with an only number formatting: scientific - {mass} / {mass : e2}", 424 | args: map[string]any{"mass": 191.0784}, 425 | expected: "This is the text with an only number formatting: scientific - 191.0784 / 1.91e+02", 426 | }, 427 | "numeric_test_2": { 428 | template: "This is the text with an only number formatting: binary - {bin:B} / {bin : B8}, hexadecimal - {hex:X} / {hex : X4}", 429 | args: map[string]any{"bin": 15, "hex": 250}, 430 | expected: "This is the text with an only number formatting: binary - 1111 / 00001111, hexadecimal - fa / 00fa", 431 | }, 432 | "numeric_test_3": { 433 | template: "This is the text with an only number formatting: decimal - {float:F} / {float : F4} / {float:F8}", 434 | args: map[string]any{"float": 10.5467890}, 435 | expected: "This is the text with an only number formatting: decimal - 10.546789 / 10.5468 / 10.54678900", 436 | }, 437 | "list_with_default_sep": { 438 | template: "This is a list(slice) test: {list:L}", 439 | args: map[string]any{"list": []any{"s1", "s2", "s3"}}, 440 | expected: "This is a list(slice) test: s1,s2,s3", 441 | }, 442 | "list_with_dash_sep": { 443 | template: "This is a list(slice) test: {list:L-}", 444 | args: map[string]any{"list": []any{"s1", "s2", "s3"}}, 445 | expected: "This is a list(slice) test: s1-s2-s3", 446 | }, 447 | "list_with_space_sep": { 448 | template: "This is a list(slice) test: {list:L }", 449 | args: map[string]any{"list": []any{"s1", "s2", "s3"}}, 450 | expected: "This is a list(slice) test: s1 s2 s3", 451 | }, 452 | } { 453 | t.Run(name, func(t *testing.T) { 454 | assert.Equal(t, test.expected, stringFormatter.FormatComplex(test.template, test.args)) 455 | }) 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /formatter.go: -------------------------------------------------------------------------------- 1 | package stringFormatter 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | const argumentFormatSeparator = ":" 10 | const bytesPerArgDefault = 16 11 | 12 | type processingState int 13 | 14 | const charAnalyzeState processingState = 1 15 | const segmentBeginDetectionState processingState = 2 16 | const segmentEndDetectionState processingState = 3 17 | 18 | // Format 19 | /* Func that makes string formatting from template 20 | * It differs from above function only by generic interface that allow to use only primitive data types: 21 | * - integers (int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uin64) 22 | * - floats (float32, float64) 23 | * - boolean 24 | * - string 25 | * - complex 26 | * - objects 27 | * This function defines format automatically 28 | * Parameters 29 | * - template - string that contains template 30 | * - args - values that are using for formatting with template 31 | * Returns formatted string 32 | */ 33 | func Format(template string, args ...any) string { 34 | if args == nil { 35 | return template 36 | } 37 | 38 | start := strings.Index(template, "{") 39 | if start < 0 { 40 | return template 41 | } 42 | 43 | templateLen := len(template) 44 | formattedStr := &strings.Builder{} 45 | argsLen := bytesPerArgDefault * len(args) 46 | formattedStr.Grow(templateLen + argsLen + 1) 47 | j := -1 //nolint:ineffassign 48 | i := start // ??? 49 | repeatingOpenBrackets := 0 50 | repeatingOpenBracketsCollected := false 51 | repeatingCloseBrackets := 0 52 | prevCloseBracketIndex := 0 53 | copyWithBrackets := false 54 | 55 | formattedStr.WriteString(template[:start]) 56 | state := charAnalyzeState 57 | for { 58 | // infinite loop, state changes on template string symbols processing, initially 59 | // we have charAnalyzeState state 60 | if state == charAnalyzeState { 61 | // this state is a space between segments 62 | // 1.1 remember j to WriteStr from j to i 63 | if j < 0 { 64 | j = i 65 | } 66 | 67 | if template[i] == '{' && i <= templateLen-2 { 68 | formattedStr.WriteString(template[j:i]) 69 | // 1.2 using j to remember a start of possible segment 70 | j = i 71 | state = segmentBeginDetectionState 72 | repeatingOpenBracketsCollected = false 73 | repeatingOpenBrackets = 1 74 | } 75 | 76 | if i == templateLen-1 { 77 | state = segmentEndDetectionState 78 | } 79 | } else { 80 | if state == segmentBeginDetectionState { 81 | // segment could be complicated: 82 | if template[i] == '{' { 83 | // we are not dealing with segment, if there are symbols between { and { 84 | if template[i-1] != '{' { 85 | state = charAnalyzeState 86 | // skip increment i, process in charAnalyzeState 87 | continue 88 | } 89 | if !repeatingOpenBracketsCollected { 90 | repeatingOpenBrackets++ 91 | } 92 | } else { 93 | repeatingOpenBracketsCollected = true 94 | } 95 | // 1. JSON object, therefore we skip it 96 | // 2. multiple nested seg {{{ and non-equal or equal number of closing brackets 97 | if template[i] == '}' { 98 | state = segmentEndDetectionState 99 | repeatingCloseBrackets = 1 100 | prevCloseBracketIndex = i 101 | } 102 | 103 | // we started to detect, but not finished yet 104 | if i == templateLen-1 { 105 | state = segmentEndDetectionState 106 | } 107 | } else { 108 | if state == segmentEndDetectionState { 109 | if template[i] != '}' || // end of the segment 110 | i == templateLen-1 { // end of the line 111 | if i == templateLen-1 { 112 | // we didn't process close bracket symbol in previous states, or in this state in diff branch 113 | if template[i] == '}' { 114 | if prevCloseBracketIndex != i { 115 | repeatingCloseBrackets++ 116 | } 117 | } 118 | } 119 | 120 | copyWithBrackets = false 121 | delta := repeatingOpenBrackets - repeatingCloseBrackets 122 | // 1. Handle brackets before parts with equal number of brackets 123 | if delta > 0 { 124 | // Write { delta times 125 | for z := 0; z < delta; z++ { 126 | formattedStr.WriteByte('{') 127 | } 128 | j += delta 129 | } 130 | // 2. Handle segment {..{arg}..} with equal number of brackets 131 | // 2.1 Multiple curly brackets handler 132 | isEven := (repeatingOpenBrackets % 2) == 0 133 | // single - {argNumberStr} handles by replace argNumber by data from list. double {{argNumberStr}} produces {argNumberStr} 134 | // triple - prof 135 | segmentPrecedingBrackets := repeatingOpenBrackets / 2 136 | 137 | if !isEven { 138 | segmentPrecedingBrackets = (repeatingOpenBrackets - 1) / 2 139 | } 140 | 141 | for z := 0; z < segmentPrecedingBrackets; z++ { 142 | formattedStr.WriteByte('{') 143 | } 144 | 145 | startIndex := j + repeatingOpenBrackets 146 | endIndex := i - repeatingCloseBrackets 147 | // don't like this, this is a shit 148 | if i == templateLen-1 { 149 | // we add endIndex +1 because selection at the mid of template line assumes that 150 | // processes segment at the next symbol i+1, but at the end of line we can't process i+1 151 | // therefore we manipulate selection indexes but ONLY in case when segment at the end of template 152 | if !(repeatingOpenBrackets > 0 && template[templateLen-1] != '}') { 153 | endIndex += 1 154 | } 155 | } 156 | argNumberStr := template[startIndex:endIndex] 157 | // 2.2 Segment formatting 158 | if !isEven { 159 | j += repeatingOpenBrackets - 1 160 | var argNumber int 161 | var err error 162 | var argFormatOptions string 163 | if len(argNumberStr) == 1 { 164 | // this calculation makes work a little faster than AtoI 165 | argNumber = int(argNumberStr[0] - '0') 166 | //rawWrite = false 167 | } else { 168 | argNumber = -1 169 | // Here we are going to process argument either with additional formatting or not 170 | // i.e. 0 for arg without formatting && 0:format for an argument wit formatting 171 | // todo(UMV): we could format json or yaml here ... 172 | formatOptionIndex := strings.Index(argNumberStr, argumentFormatSeparator) 173 | // formatOptionIndex can't be == 0, because 0 is a position of arg number 174 | if formatOptionIndex > 0 { 175 | // trimmed was down later due to we could format list with space separator 176 | argFormatOptions = argNumberStr[formatOptionIndex+1:] 177 | argNumberStrPart := argNumberStr[:formatOptionIndex] 178 | argNumber, err = strconv.Atoi(strings.Trim(argNumberStrPart, " ")) 179 | if err == nil { 180 | argNumberStr = argNumberStrPart 181 | //rawWrite = false 182 | } 183 | 184 | // make formatting option str for further pass to an argument 185 | } 186 | if argNumber < 0 { 187 | argNumber, err = strconv.Atoi(argNumberStr) 188 | } 189 | } 190 | 191 | if (err == nil || (argFormatOptions != "" && !repeatingOpenBracketsCollected)) && 192 | len(args) > argNumber { 193 | // get number from placeholder 194 | strVal := getItemAsStr(&args[argNumber], &argFormatOptions) 195 | formattedStr.WriteString(strVal) 196 | } else { 197 | copyWithBrackets = true 198 | if i < templateLen-1 { 199 | formattedStr.WriteString(template[j:i]) 200 | } else { 201 | // if i is the last symbol in template line, we should take i+1 202 | formattedStr.WriteString(template[j : i+1]) //template 203 | } 204 | 205 | } 206 | } else { 207 | formattedStr.WriteString(argNumberStr) 208 | } 209 | 210 | for z := 0; z < segmentPrecedingBrackets; z++ { 211 | formattedStr.WriteByte('}') 212 | } 213 | 214 | // 3. Handle brackets after segment 215 | if !copyWithBrackets { 216 | for z := 0; z < delta*-1; z++ { 217 | formattedStr.WriteByte('}') 218 | } 219 | } 220 | 221 | state = charAnalyzeState 222 | if i == templateLen-1 { 223 | // this is for writing last symbol that follows after segment at the end of template 224 | if endIndex < templateLen-1 && template[templateLen-1] != '}' { 225 | formattedStr.WriteByte(template[templateLen-1]) 226 | } 227 | break 228 | } else { 229 | j = i 230 | } 231 | repeatingOpenBrackets = 0 232 | repeatingCloseBrackets = 0 233 | } else { 234 | repeatingCloseBrackets++ 235 | prevCloseBracketIndex = i 236 | } 237 | } 238 | } 239 | } 240 | // sometimes we are using continue to move to another state within current i value 241 | if i < templateLen-1 { 242 | i++ 243 | } 244 | } 245 | 246 | return formattedStr.String() 247 | } 248 | 249 | // FormatComplex 250 | /* Function that format text using more complex templates contains string literals i.e "Hello {username} here is our application {appname}" 251 | * Parameters 252 | * - template - string that contains template 253 | * - args - values (dictionary: string key - any value) that are using for formatting with template 254 | * Returns formatted string 255 | */ 256 | func FormatComplex(template string, args map[string]any) string { 257 | if args == nil { 258 | return template 259 | } 260 | 261 | start := strings.Index(template, "{") 262 | if start < 0 { 263 | return template 264 | } 265 | 266 | templateLen := len(template) 267 | formattedStr := &strings.Builder{} 268 | argsLen := bytesPerArgDefault * len(args) 269 | formattedStr.Grow(templateLen + argsLen + 1) 270 | j := -1 //nolint:ineffassign 271 | i := start // ??? 272 | repeatingOpenBrackets := 0 273 | repeatingOpenBracketsCollected := false 274 | repeatingCloseBrackets := 0 275 | prevCloseBracketIndex := 0 276 | copyWithBrackets := false 277 | 278 | formattedStr.WriteString(template[:start]) 279 | state := charAnalyzeState 280 | for { 281 | // infinite loop, state changes on template string symbols processing, initially 282 | // we have charAnalyzeState state 283 | if state == charAnalyzeState { 284 | // this state is a space between segments 285 | // 1.1 remember j to WriteStr from j to i 286 | if j < 0 { 287 | j = i 288 | } 289 | 290 | if template[i] == '{' && i <= templateLen-2 { 291 | formattedStr.WriteString(template[j:i]) 292 | // 1.2 using j to remember a start of possible segment 293 | j = i 294 | state = segmentBeginDetectionState 295 | repeatingOpenBracketsCollected = false 296 | repeatingOpenBrackets = 1 297 | } 298 | 299 | if i == templateLen-1 { 300 | state = segmentEndDetectionState 301 | } 302 | } else { 303 | if state == segmentBeginDetectionState { 304 | // segment could be complicated: 305 | if template[i] == '{' { 306 | // we are not dealing with segment, if there are symbols between { and { 307 | if template[i-1] != '{' { 308 | state = charAnalyzeState 309 | // skip increment i, process in charAnalyzeState 310 | continue 311 | } 312 | if !repeatingOpenBracketsCollected { 313 | repeatingOpenBrackets++ 314 | } 315 | } else { 316 | repeatingOpenBracketsCollected = true 317 | } 318 | // 1. JSON object, therefore we skip it 319 | // 2. multiple nested seg {{{ and non-equal or equal number of closing brackets 320 | if template[i] == '}' { 321 | state = segmentEndDetectionState 322 | repeatingCloseBrackets = 1 323 | prevCloseBracketIndex = i 324 | } 325 | 326 | // we started to detect, but not finished yet 327 | if i == templateLen-1 { 328 | state = segmentEndDetectionState 329 | } 330 | } else { 331 | if state == segmentEndDetectionState { 332 | if template[i] != '}' || // end of the segment 333 | i == templateLen-1 { // end of the line 334 | if i == templateLen-1 { 335 | // we didn't process close bracket symbol in previous states, or in this state in diff branch 336 | if template[i] == '}' { 337 | if prevCloseBracketIndex != i { 338 | repeatingCloseBrackets++ 339 | } 340 | } 341 | } 342 | 343 | copyWithBrackets = false 344 | delta := repeatingOpenBrackets - repeatingCloseBrackets 345 | // 1. Handle brackets before parts with equal number of brackets 346 | if delta > 0 { 347 | // Write { delta times 348 | for z := 0; z < delta; z++ { 349 | formattedStr.WriteByte('{') 350 | } 351 | j += delta 352 | } 353 | // 2. Handle segment {..{arg}..} with equal number of brackets 354 | // 2.1 Multiple curly brackets handler 355 | isEven := (repeatingOpenBrackets % 2) == 0 356 | // single - {argNumberStr} handles by replace argNumber by data from list. double {{argNumberStr}} produces {argNumberStr} 357 | // triple - prof 358 | segmentPrecedingBrackets := repeatingOpenBrackets / 2 359 | 360 | if !isEven { 361 | segmentPrecedingBrackets = (repeatingOpenBrackets - 1) / 2 362 | } 363 | 364 | for z := 0; z < segmentPrecedingBrackets; z++ { 365 | formattedStr.WriteByte('{') 366 | } 367 | 368 | startIndex := j + repeatingOpenBrackets 369 | endIndex := i - repeatingCloseBrackets 370 | // don't like this, this is a shit 371 | if i == templateLen-1 { 372 | // we add endIndex +1 because selection at the mid of template line assumes that 373 | // processes segment at the next symbol i+1, but at the end of line we can't process i+1 374 | // therefore we manipulate selection indexes but ONLY in case when segment at the end of template 375 | if !(repeatingOpenBrackets > 0 && template[templateLen-1] != '}') { 376 | endIndex += 1 377 | } 378 | } 379 | argKeyStr := template[startIndex:endIndex] 380 | argKey := argKeyStr 381 | 382 | // 2.2 Segment formatting 383 | if !isEven { 384 | j += repeatingOpenBrackets - 1 385 | var argFormatOptions string 386 | // var argNumberStrPart string 387 | 388 | // Here we are going to process argument either with additional formatting or not 389 | // i.e. 0 for arg without formatting && 0:format for an argument wit formatting 390 | // todo(UMV): we could format json or yaml here ... 391 | formatOptionIndex := strings.Index(argKeyStr, argumentFormatSeparator) 392 | // formatOptionIndex can't be == 0, because 0 is a position of arg number 393 | if formatOptionIndex > 0 { 394 | // trimmed was down later due to we could format list with space separator 395 | argFormatOptions = argKeyStr[formatOptionIndex+1:] 396 | argKey = argKeyStr[:formatOptionIndex] 397 | } 398 | 399 | arg, ok := args[argKey] 400 | if !ok { 401 | formatOptionIndex = strings.Index(argKeyStr, argumentFormatSeparator) 402 | if formatOptionIndex >= 0 { 403 | // argFormatOptions = strings.Trim(argNumberStr[formatOptionIndex+1:], " ") 404 | argFormatOptions = argKeyStr[formatOptionIndex+1:] 405 | argKey = strings.Trim(argKey[:formatOptionIndex], " ") 406 | } 407 | 408 | arg, ok = args[argKey] 409 | } 410 | if ok || argFormatOptions != "" { 411 | // get number from placeholder 412 | strVal := "" 413 | if arg != nil { 414 | strVal = getItemAsStr(&arg, &argFormatOptions) 415 | } else { 416 | copyWithBrackets = true 417 | if i < templateLen-1 { 418 | formattedStr.WriteString(template[j:i]) 419 | } else { 420 | // if i is the last symbol in template line, we should take i+1 421 | formattedStr.WriteString(template[j : i+1]) //template 422 | } 423 | } 424 | formattedStr.WriteString(strVal) 425 | } else { 426 | copyWithBrackets = true 427 | if i < templateLen-1 { 428 | formattedStr.WriteString(template[j:i]) 429 | } else { 430 | // if i is the last symbol in template line, we should take i+1 431 | formattedStr.WriteString(template[j : i+1]) //template 432 | } 433 | } 434 | 435 | } else { 436 | formattedStr.WriteString(argKeyStr) 437 | } 438 | 439 | for z := 0; z < segmentPrecedingBrackets; z++ { 440 | formattedStr.WriteByte('}') 441 | } 442 | 443 | // 3. Handle brackets after segment 444 | if !copyWithBrackets { 445 | for z := 0; z < delta*-1; z++ { 446 | formattedStr.WriteByte('}') 447 | } 448 | } 449 | 450 | state = charAnalyzeState 451 | if i == templateLen-1 { 452 | // this is for writing last symbol that follows after segment at the end of template 453 | if endIndex < templateLen-1 && template[templateLen-1] != '}' { 454 | formattedStr.WriteByte(template[templateLen-1]) 455 | } 456 | break 457 | } else { 458 | j = i 459 | } 460 | repeatingOpenBrackets = 0 461 | repeatingCloseBrackets = 0 462 | } else { 463 | repeatingCloseBrackets++ 464 | prevCloseBracketIndex = i 465 | } 466 | } 467 | } 468 | } 469 | // sometimes we are using continue to move to another state within current i value 470 | if i < templateLen-1 { 471 | i++ 472 | } 473 | } 474 | 475 | return formattedStr.String() 476 | } 477 | 478 | func getItemAsStr(item *any, itemFormat *string) string { 479 | base := 10 480 | var floatFormat byte = 'f' 481 | precision := -1 482 | var preparedArgFormat string 483 | var argStr string 484 | postProcessingRequired := false 485 | intNumberFormat := false 486 | floatNumberFormat := false 487 | 488 | if itemFormat != nil && len(*itemFormat) > 0 { 489 | /* for numbers there are following formats: 490 | * d(D) - decimal 491 | * b(B) - binary 492 | * f(F) - fixed point i.e {0:F}, 10.5467890 -> 10.546789 ; {0:F4}, 10.5467890 -> 10.5468 493 | * e(E) - exponential - float point with scientific format {0:E2}, 191.0784 -> 1.91e+02 494 | * x(X) - hexadecimal i.e. {0:X}, 250 -> fa ; {0:X4}, 250 -> 00fa 495 | * p(P) - percent i.e. {0:P100}, 12 -> 12% 496 | * Following formats are not supported yet: 497 | * 1. c(C) currency it requires also country code 498 | * 2. g(G),and others with locales 499 | * 3. f(F) - fixed point, {0,F4}, 123.15 -> 123.1500 500 | * OUR own addition: 501 | * 1. O(o) - octahedral number format 502 | */ 503 | // preparedArgFormat is trimmed format, L type could contain spaces 504 | preparedArgFormat = strings.Trim(*itemFormat, " ") 505 | postProcessingRequired = len(preparedArgFormat) > 1 506 | 507 | switch rune(preparedArgFormat[0]) { 508 | case 'd', 'D': 509 | base = 10 510 | intNumberFormat = true 511 | case 'x', 'X': 512 | base = 16 513 | intNumberFormat = true 514 | case 'o', 'O': 515 | base = 8 516 | intNumberFormat = true 517 | case 'b', 'B': 518 | base = 2 519 | intNumberFormat = true 520 | case 'e', 'E', 'f', 'F': 521 | if rune(preparedArgFormat[0]) == 'e' || rune(preparedArgFormat[0]) == 'E' { 522 | floatFormat = 'e' 523 | } 524 | // precision was passed, take [1:end], extract precision 525 | if postProcessingRequired { 526 | precisionStr := preparedArgFormat[1:] 527 | precisionVal, err := strconv.Atoi(precisionStr) 528 | if err == nil { 529 | precision = precisionVal 530 | } 531 | } 532 | postProcessingRequired = false 533 | floatNumberFormat = floatFormat == 'f' 534 | 535 | case 'p', 'P': 536 | // percentage processes here ... 537 | if postProcessingRequired { 538 | dividerStr := preparedArgFormat[1:] 539 | dividerVal, err := strconv.ParseFloat(dividerStr, 32) 540 | if err == nil { 541 | // 1. Convert arg to float 542 | var floatVal float64 543 | switch (*item).(type) { 544 | case float64: 545 | floatVal = (*item).(float64) 546 | case int: 547 | floatVal = float64((*item).(int)) 548 | default: 549 | floatVal = 0 550 | } 551 | // 2. Divide arg / divider and multiply by 100 552 | percentage := (floatVal / dividerVal) * 100 553 | return strconv.FormatFloat(percentage, floatFormat, 2, 64) 554 | } 555 | } 556 | // l(L) is for list(slice) 557 | case 'l', 'L': 558 | separator := "," 559 | if len(*itemFormat) > 1 { 560 | separator = (*itemFormat)[1:] 561 | } 562 | 563 | // slice processing converting to {item}{delimiter}{item}{delimiter}{item} 564 | slice, ok := (*item).([]any) 565 | //nolint:ineffassign 566 | if ok { 567 | if len(slice) == 1 { 568 | // this is because slice in 0 item contains another slice, we should take it 569 | slice, _ = slice[0].([]any) 570 | } 571 | return SliceToString(&slice, &separator) 572 | } else { 573 | return convertSliceToStrWithTypeDiscover(item, &separator) 574 | } 575 | case 'c', 'C': 576 | // c means code for apply stringFormatter.SetStyle() 577 | /* code formatting could be set as follows: 578 | * 1. {0:c:Camel} - stands for camel case with Capital letter at begin 579 | * 2. {0:c:camel} - stands for camel case with Capital letter at begin 580 | * 3. {0:c:Kebab}, {0:c:kebab}, {0:c:KEBAB} - 3 ways of Kebab formatting: first lower case with start Capital letter 581 | * 4. {0:c:Snake}, {0:c:snake}, {0:c:SNAKE} - 3 ways of Snake formatting: first lower case with start Capital letter 582 | */ 583 | formatSubParts := strings.Split(*itemFormat, ":") 584 | if len(formatSubParts) > 1 { 585 | format, firstSymbolCase, textCase := GetFormattingStyleOptions(formatSubParts[1]) 586 | val := (*item).([]interface{}) 587 | itemStr := val[0].(string) 588 | return SetStyle(&itemStr, format, firstSymbolCase, textCase) 589 | } 590 | default: 591 | base = 10 592 | } 593 | } 594 | 595 | switch v := (*item).(type) { 596 | case string: 597 | argStr = v 598 | case int8: 599 | argStr = strconv.FormatInt(int64(v), base) 600 | case int16: 601 | argStr = strconv.FormatInt(int64(v), base) 602 | case int32: 603 | argStr = strconv.FormatInt(int64(v), base) 604 | case int64: 605 | argStr = strconv.FormatInt(v, base) 606 | case int: 607 | argStr = strconv.FormatInt(int64(v), base) 608 | case uint8: 609 | argStr = strconv.FormatUint(uint64(v), base) 610 | case uint16: 611 | argStr = strconv.FormatUint(uint64(v), base) 612 | case uint32: 613 | argStr = strconv.FormatUint(uint64(v), base) 614 | case uint64: 615 | argStr = strconv.FormatUint(v, base) 616 | case uint: 617 | argStr = strconv.FormatUint(uint64(v), base) 618 | case bool: 619 | argStr = strconv.FormatBool(v) 620 | case float32: 621 | argStr = strconv.FormatFloat(float64(v), floatFormat, precision, 32) 622 | case float64: 623 | argStr = strconv.FormatFloat(v, floatFormat, precision, 64) 624 | default: 625 | argStr = fmt.Sprintf("%v", v) 626 | } 627 | 628 | if !postProcessingRequired { 629 | return argStr 630 | } 631 | 632 | // 1. If integer numbers add filling 633 | if intNumberFormat { 634 | symbolsStr := preparedArgFormat[1:] 635 | symbolsStrVal, err := strconv.Atoi(symbolsStr) 636 | if err == nil { 637 | symbolsToAdd := symbolsStrVal - len(argStr) 638 | if symbolsToAdd > 0 { 639 | advArgStr := strings.Builder{} 640 | advArgStr.Grow(len(argStr) + symbolsToAdd + 1) 641 | 642 | for i := 0; i < symbolsToAdd; i++ { 643 | advArgStr.WriteByte('0') 644 | } 645 | advArgStr.WriteString(argStr) 646 | return advArgStr.String() 647 | } 648 | } 649 | } 650 | 651 | if floatNumberFormat && precision > 0 { 652 | pointIndex := strings.Index(argStr, ".") 653 | if pointIndex > 0 { 654 | advArgStr := strings.Builder{} 655 | advArgStr.Grow(len(argStr) + precision + 1) 656 | advArgStr.WriteString(argStr) 657 | numberOfSymbolsAfterPoint := len(argStr) - (pointIndex + 1) 658 | for i := numberOfSymbolsAfterPoint; i < precision; i++ { 659 | advArgStr.WriteByte(0) 660 | } 661 | return advArgStr.String() 662 | } 663 | } 664 | 665 | return argStr 666 | } 667 | 668 | func convertSliceToStrWithTypeDiscover(slice *any, separator *string) string { 669 | // 1. attempt to convert to int 670 | iSlice, ok := (*slice).([]int) 671 | if ok { 672 | return SliceSameTypeToString(&iSlice, separator) 673 | } 674 | 675 | // 2. attempt to convert to string 676 | sSlice, ok := (*slice).([]string) 677 | if ok { 678 | return SliceSameTypeToString(&sSlice, separator) 679 | } 680 | 681 | // 3. attempt to convert to float64 682 | f64Slice, ok := (*slice).([]float64) 683 | if ok { 684 | return SliceSameTypeToString(&f64Slice, separator) 685 | } 686 | 687 | // 4. attempt to convert to float32 688 | f32Slice, ok := (*slice).([]float32) 689 | if ok { 690 | return SliceSameTypeToString(&f32Slice, separator) 691 | } 692 | 693 | // 5. attempt to convert to bool 694 | bSlice, ok := (*slice).([]bool) 695 | if ok { 696 | return SliceSameTypeToString(&bSlice, separator) 697 | } 698 | 699 | // 6. attempt to convert to int64 700 | i64Slice, ok := (*slice).([]int64) 701 | if ok { 702 | return SliceSameTypeToString(&i64Slice, separator) 703 | } 704 | 705 | // 7. attempt to convert to uint 706 | uiSlice, ok := (*slice).([]uint) 707 | if ok { 708 | return SliceSameTypeToString(&uiSlice, separator) 709 | } 710 | 711 | // 8. attempt to convert to int32 712 | i32Slice, ok := (*slice).([]int32) 713 | if ok { 714 | return SliceSameTypeToString(&i32Slice, separator) 715 | } 716 | 717 | // default way ... 718 | return fmt.Sprintf("%v", *slice) 719 | } 720 | --------------------------------------------------------------------------------