├── 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 |
5 |
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 | 
8 | 
9 | 
10 | 
11 | 
12 |
13 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------