From ecdd5a0a1ec1701aada4f33473c14c38c24824d1 Mon Sep 17 00:00:00 2001 From: Poletansky Viktor Date: Tue, 18 Jul 2023 20:34:38 +0300 Subject: [PATCH 1/2] dev: init dev: rename dev: simplify tests dev: prettify test --- README.md | 41 +-- formatter.go | 233 +++++++-------- formatter_benchmark_test.go | 38 ++- formatter_test.go | 282 ++++++++++-------- maptostring.go | 44 +++ ...hmark_test.go => maptostring_bench_test.go | 12 +- maptostring_test.go | 75 +++++ text_utilities_test.go | 37 --- text_utils.go | 42 --- 9 files changed, 436 insertions(+), 368 deletions(-) create mode 100644 maptostring.go rename text_utils_benchmark_test.go => maptostring_bench_test.go (72%) create mode 100644 maptostring_test.go delete mode 100644 text_utilities_test.go delete mode 100644 text_utils.go diff --git a/README.md b/README.md index d4056f7..9edb640 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ i.e. you have following template: `"Hello {0}, we are greeting you here: {1}!"` if you call Format with args "manager" and "salesApp" : ```go -formattedStr := Format("Hello {0}, we are greeting you here: {1}!", "manager", "salesApp") +formattedStr := stringFormatter.Format("Hello {0}, we are greeting you here: {1}!", "manager", "salesApp") ``` you get string `"Hello manager, we are greeting you here: salesApp!"` @@ -44,15 +44,17 @@ you get string `"Hello manager, we are greeting you here: salesApp!"` i.e. you have following template: `"Hello {user} what are you doing here {app} ?"` -if you call `FormatComplex` with args `"vpupkin"` and `"mn_console"` `FormatComplex("Hello {user} what are you doing here {app} ?", map[string]interface{}{"user":"vpupkin", "app":"mn_console"})` +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"})` you get string `"Hello vpupkin what are you doing here mn_console ?"` another example is: ```go - strFormatResult = FormatComplex("Current app settings are: ipAddr: {ipaddr}, port: {port}, use ssl: {ssl}.", - map[string]interface{}{"ipaddr":"127.0.0.1", "port":5432, "ssl":false}) +strFormatResult = stringFormatter.FormatComplex( + "Current app settings are: ipAddr: {ipaddr}, port: {port}, use ssl: {ssl}.", + map[string]any{"ipaddr":"127.0.0.1", "port":5432, "ssl":false}, +) ``` a result will be: `"Current app settings are: ipAddr: 127.0.0.1, port: 5432, use ssl: false."`` @@ -66,23 +68,24 @@ benchmark could be running using following commands from command line: #### 2.1 Map to string utility -Map to string function allow to convert map to string using one of predefined line format: -* `key => value` -* `key : value` -* `value` +`MapToString` function allows to convert map with primitive key to string using format, including key and value, e.g.: +* `{key} => {value}` +* `{key} : {value}` +* `{value}` -For example see code from test (`text_utils_test.go`): +For example: ```go -options := map[string]interface{}{ - "connectTimeout": 1000, - "useSsl": true, - "login": "sa", - "password": "sa", - } - - str := MapToString(&options, KeyValueWithSemicolonSepFormat, ", ") - assert.True(t, len(str) > 0) - assert.Equal(t, "connectTimeout : 1000, useSsl : true, login : sa, password : sa", str) +options := map[string]any{ + "connectTimeout": 1000, + "useSsl": true, + "login": "sa", + "password": "sa", +} + +str := stringFormatter.MapToString(&options, "{key} : {value}", ", ") +// NOTE: order of key-value pairs is not guranteed though +// str will be something like: +"connectTimeout : 1000, useSsl : true, login : sa, password : sa" ``` #### 2.2 Benchmarks of the MapToStr function diff --git a/formatter.go b/formatter.go index 477dc5d..227b24c 100644 --- a/formatter.go +++ b/formatter.go @@ -21,80 +21,81 @@ import ( * - args - values that are using for formatting with template * Returns formatted string */ -func Format(template string, args ...interface{}) string { +func Format(template string, args ...any) string { if args == nil { return template } - templateLen := len(template) - var formattedStr = &strings.Builder{} - formattedStr.Grow(templateLen + 22*len(args)) - j := -1 start := strings.Index(template, "{") if start < 0 { return template } + templateLen := len(template) + formattedStr := &strings.Builder{} + formattedStr.Grow(templateLen + 22*len(args)) + j := -1 //nolint:ineffassign + formattedStr.WriteString(template[:start]) for i := start; i < templateLen; i++ { - if template[i] == '{' { // possibly it is a template placeholder if i == templateLen-1 { break } + if template[i+1] == '{' { // todo: umv: this not considering {{0}} formattedStr.WriteByte('{') continue - } else { - // find end of placeholder - j = i + 2 - for { - if j >= templateLen { - break - } - if template[j] == '{' { - // multiple nested curly brackets ... - formattedStr.WriteString(template[i:j]) - i = j - } - if template[j] == '}' { - break - } - j++ + } + // find end of placeholder + j = i + 2 + for { + if j >= templateLen { + break } - // double curly brackets processed here, convert {{N}} -> {N} - // so we catch here {{N} - if j+1 < templateLen && template[j+1] == '}' && template[i-1] == '{' { - formattedStr.WriteString(template[i+1 : j+1]) - i = j + 1 - } else { - argNumberStr := template[i+1 : j] - var argNumber int - var err error - if len(argNumberStr) == 1 { - // this makes work a little faster then AtoI - argNumber = int(argNumberStr[0] - '0') - } else { - argNumber, err = strconv.Atoi(argNumberStr) - } - //argNumber, err := strconv.Atoi(argNumberStr) - if err == nil && len(args) > argNumber { - // get number from placeholder - strVal := getItemAsStr(&args[argNumber]) - formattedStr.WriteString(strVal) - } else { - formattedStr.WriteByte('{') - formattedStr.WriteString(argNumberStr) - formattedStr.WriteByte('}') - } + if template[j] == '{' { + // multiple nested curly brackets ... + formattedStr.WriteString(template[i:j]) i = j } - } + if template[j] == '}' { + break + } + + j++ + } + // double curly brackets processed here, convert {{N}} -> {N} + // so we catch here {{N} + if j+1 < templateLen && template[j+1] == '}' && template[i-1] == '{' { + formattedStr.WriteString(template[i+1 : j+1]) + i = j + 1 + } else { + argNumberStr := template[i+1 : j] + var argNumber int + var err error + if len(argNumberStr) == 1 { + // this makes work a little faster than AtoI + argNumber = int(argNumberStr[0] - '0') + } else { + argNumber, err = strconv.Atoi(argNumberStr) + } + //argNumber, err := strconv.Atoi(argNumberStr) + if err == nil && len(args) > argNumber { + // get number from placeholder + strVal := getItemAsStr(&args[argNumber]) + formattedStr.WriteString(strVal) + } else { + formattedStr.WriteByte('{') + formattedStr.WriteString(argNumberStr) + formattedStr.WriteByte('}') + } + i = j + } } else { - j = i + j = i //nolint:ineffassign formattedStr.WriteByte(template[i]) } } @@ -109,72 +110,70 @@ func Format(template string, args ...interface{}) string { * - args - values (dictionary: string key - any value) that are using for formatting with template * Returns formatted string */ -func FormatComplex(template string, args map[string]interface{}) string { +func FormatComplex(template string, args map[string]any) string { if args == nil { return template } - templateLen := len(template) - var formattedStr = &strings.Builder{} - formattedStr.Grow(templateLen + 22*len(args)) - j := -1 start := strings.Index(template, "{") if start < 0 { return template } + templateLen := len(template) + formattedStr := &strings.Builder{} + formattedStr.Grow(templateLen + 22*len(args)) + j := -1 //nolint:ineffassign formattedStr.WriteString(template[:start]) for i := start; i < templateLen; i++ { - if template[i] == '{' { // possibly it is a template placeholder if i == templateLen-1 { break } + if template[i+1] == '{' { // todo: umv: this not considering {{0}} formattedStr.WriteByte('{') continue - } else { - // find end of placeholder - j = i + 2 - for { - if j >= templateLen { - break - } - if template[j] == '{' { - // multiple nested curly brackets ... - formattedStr.WriteString(template[i:j]) - i = j - } - if template[j] == '}' { - break - } - j++ - } - // double curly brackets processed here, convert {{N}} -> {N} - // so we catch here {{N} - if j+1 < templateLen && template[j+1] == '}' { + } - formattedStr.WriteString(template[i+1 : j+1]) - i = j + 1 - } else { - argNumberStr := template[i+1 : j] - arg, ok := args[argNumberStr] - if ok { - // get number from placeholder - strVal := getItemAsStr(&arg) - formattedStr.WriteString(strVal) - } else { - formattedStr.WriteByte('{') - formattedStr.WriteString(argNumberStr) - formattedStr.WriteByte('}') - } + // find end of placeholder + j = i + 2 + for { + if j >= templateLen { + break + } + if template[j] == '{' { + // multiple nested curly brackets ... + formattedStr.WriteString(template[i:j]) i = j } + if template[j] == '}' { + break + } + j++ + } + // double curly brackets processed here, convert {{N}} -> {N} + // so we catch here {{N} + if j+1 < templateLen && template[j+1] == '}' { + formattedStr.WriteString(template[i+1 : j+1]) + i = j + 1 + } else { + argNumberStr := template[i+1 : j] + arg, ok := args[argNumberStr] + if ok { + // get number from placeholder + strVal := getItemAsStr(&arg) + formattedStr.WriteString(strVal) + } else { + formattedStr.WriteByte('{') + formattedStr.WriteString(argNumberStr) + formattedStr.WriteByte('}') + } + i = j } - } else { - j = i + j = i //nolint:ineffassign formattedStr.WriteByte(template[i]) } } @@ -183,55 +182,37 @@ func FormatComplex(template string, args map[string]interface{}) string { } // todo: umv: impl format passing as param -func getItemAsStr(item *interface{}) string { - var strVal string - //var err error - switch (*item).(type) { +func getItemAsStr(item *any) string { + switch v := (*item).(type) { case string: - strVal = (*item).(string) - break + return v case int8: - strVal = strconv.FormatInt(int64((*item).(int8)), 10) - break + return strconv.FormatInt(int64(v), 10) case int16: - strVal = strconv.FormatInt(int64((*item).(int16)), 10) - break + return strconv.FormatInt(int64(v), 10) case int32: - strVal = strconv.FormatInt(int64((*item).(int32)), 10) - break + return strconv.FormatInt(int64(v), 10) case int64: - strVal = strconv.FormatInt((*item).(int64), 10) - break + return strconv.FormatInt(v, 10) case int: - strVal = strconv.FormatInt(int64((*item).(int)), 10) - break + return strconv.FormatInt(int64(v), 10) case uint8: - strVal = strconv.FormatUint(uint64((*item).(uint8)), 10) - break + return strconv.FormatUint(uint64(v), 10) case uint16: - strVal = strconv.FormatUint(uint64((*item).(uint16)), 10) - break + return strconv.FormatUint(uint64(v), 10) case uint32: - strVal = strconv.FormatUint(uint64((*item).(uint32)), 10) - break + return strconv.FormatUint(uint64(v), 10) case uint64: - strVal = strconv.FormatUint((*item).(uint64), 10) - break + return strconv.FormatUint(v, 10) case uint: - strVal = strconv.FormatUint(uint64((*item).(uint)), 10) - break + return strconv.FormatUint(uint64(v), 10) case bool: - strVal = strconv.FormatBool((*item).(bool)) - break + return strconv.FormatBool(v) case float32: - strVal = strconv.FormatFloat(float64((*item).(float32)), 'f', -1, 32) - break + return strconv.FormatFloat(float64(v), 'f', -1, 32) case float64: - strVal = strconv.FormatFloat((*item).(float64), 'f', -1, 64) - break + return strconv.FormatFloat(v, 'f', -1, 64) default: - strVal = fmt.Sprintf("%v", *item) - break + return fmt.Sprintf("%v", v) } - return strVal } diff --git a/formatter_benchmark_test.go b/formatter_benchmark_test.go index 04858a3..b7c706f 100644 --- a/formatter_benchmark_test.go +++ b/formatter_benchmark_test.go @@ -8,34 +8,54 @@ import ( func BenchmarkFormat4Arg(b *testing.B) { for i := 0; i < b.N; i++ { - _ = Format("Today is : {0}, atmosphere pressure is : {1} mmHg, temperature: {2}, location: {3}", time.Now().String(), 725, -1.54, "Yekaterinburg") + _ = Format( + "Today is : {0}, atmosphere pressure is : {1} mmHg, temperature: {2}, location: {3}", + time.Now().String(), 725, -1.54, "Yekaterinburg", + ) } } func BenchmarkFmt4Arg(b *testing.B) { for i := 0; i < b.N; i++ { - _ = fmt.Sprintf("Today is : %s, atmosphere pressure is : %d mmHg, temperature: %f, location: %s", time.Now().String(), 725, -1.54, "Yekaterinburg") + _ = fmt.Sprintf( + "Today is : %s, atmosphere pressure is : %d mmHg, temperature: %f, location: %s", + time.Now().String(), 725, -1.54, "Yekaterinburg", + ) } } func BenchmarkFormat6Arg(b *testing.B) { for i := 0; i < b.N; i++ { - _ = Format("Today is : {0}, atmosphere pressure is : {1} mmHg, temperature: {2}, location: {3}, coord:{4}-{5}", - time.Now().String(), 725, -1.54, "Yekaterinburg", "64.245", "37.895") + _ = Format( + "Today is : {0}, atmosphere pressure is : {1} mmHg, temperature: {2}, location: {3}, coord:{4}-{5}", + time.Now().String(), 725, -1.54, "Yekaterinburg", "64.245", "37.895", + ) } } func BenchmarkFmt6Arg(b *testing.B) { for i := 0; i < b.N; i++ { - _ = fmt.Sprintf("Today is : %s, atmosphere pressure is : %d mmHg, temperature: %f, location: %s, coords: %s-%s", - time.Now().String(), 725, -1.54, "Yekaterinburg", "64.245", "37.895") + _ = fmt.Sprintf( + "Today is : %s, atmosphere pressure is : %d mmHg, temperature: %f, location: %s, coords: %s-%s", + time.Now().String(), 725, -1.54, "Yekaterinburg", "64.245", "37.895", + ) } } func BenchmarkFormatComplex7Arg(b *testing.B) { - args := map[string]interface{}{"temperature": -10, "location": "Yekaterinburg", "time": time.Now().String(), "pressure": 725, "humidity": 34, - "longitude": "64.245", "latitude": "35.489"} + args := map[string]any{ + "temperature": -10, + "location": "Yekaterinburg", + "time": time.Now().String(), + "pressure": 725, + "humidity": 34, + "longitude": "64.245", + "latitude": "35.489", + } for i := 0; i < b.N; i++ { - _ = FormatComplex("Today is : {time}, atmosphere pressure is : {pressure} mmHg, humidity: {humidity}, temperature: {temperature}, location: {location}, coords:{longitude}-{latitude}", args) + _ = FormatComplex( + "Today is : {time}, atmosphere pressure is : {pressure} mmHg, humidity: {humidity}, temperature: {temperature}, location: {location}, coords:{longitude}-{latitude}", + args, + ) } } diff --git a/formatter_test.go b/formatter_test.go index 74ef283..8fa3c85 100644 --- a/formatter_test.go +++ b/formatter_test.go @@ -1,145 +1,165 @@ -package stringFormatter +package stringFormatter_test import ( "errors" - "github.com/stretchr/testify/assert" "testing" -) - -func TestStrFormat(t *testing.T) { - strFormatResult := Format("Hello i am {0}, my age is {1} and i am waiting for {2}, because i am {0}!", - "Michael Ushakov (Evillord666)", "34", "\"Great Success\"") - assert.Equal(t, "Hello i am Michael Ushakov (Evillord666), my age is 34 and i am waiting for \"Great Success\", because i am Michael Ushakov (Evillord666)!", strFormatResult) - - strFormatResult = Format("We are wondering if these values would be replaced : {5}, {4}, {0}", "one", "two", "three") - assert.Equal(t, "We are wondering if these values would be replaced : {5}, {4}, one", strFormatResult) - - strFormatResult = Format("No args ... : {0}, {1}, {2}") - assert.Equal(t, "No args ... : {0}, {1}, {2}", strFormatResult) -} - -// TestStrFormatWithComplicatedText - this test represents issue with complicated text -func TestStrFormatWithComplicatedText(t *testing.T) { - address := "grpcs://127.0.0.1" - stateMachineSource := ` - { - "Comment": "Call Lambda with GRPC", - "StartAt": "CallLambdaWithGrpc", - "States": {"CallLambdaWithGrpc": {"Type": "Task", "Resource": "{0}:get ad user", "End": true}} - }` - actualSm := Format(stateMachineSource, address) - expectedSm := ` - { - "Comment": "Call Lambda with GRPC", - "StartAt": "CallLambdaWithGrpc", - "States": {"CallLambdaWithGrpc": {"Type": "Task", "Resource": "grpcs://127.0.0.1:get ad user", "End": true}} - }` - assert.Equal(t, expectedSm, actualSm) - - stateMachineSource = ` - { - "Comment": "Call Lambda with GRPC", - "StartAt": "CallLambdaWithGrpc", - "States": {"CallLambdaWithGrpc": {"Type": "Task", "Resource": "{address}:get ad user", "End": true}} - }` - actualSm = FormatComplex(stateMachineSource, map[string]interface{}{"address": address}) - expectedSm = ` - { - "Comment": "Call Lambda with GRPC", - "StartAt": "CallLambdaWithGrpc", - "States": {"CallLambdaWithGrpc": {"Type": "Task", "Resource": "grpcs://127.0.0.1:get ad user", "End": true}} - }` - assert.Equal(t, expectedSm, actualSm) -} - -func TestStrFormatWithDoubleCurlyBrackets(t *testing.T) { - strFormatResult := Format("Hello i am {{0}}, my age is {1} and i am waiting for {2}, because i am {0}!", - "Michael Ushakov (Evillord666)", "34", "\"Great Success\"") - assert.Equal(t, "Hello i am {0}, my age is 34 and i am waiting for \"Great Success\", because i am Michael Ushakov (Evillord666)!", strFormatResult) - strFormatResult = Format("At the end {{0}}", "s") - assert.Equal(t, "At the end {0}", strFormatResult) -} - -func TestStrFormatWithMultipleNestedCurlyBrackets(t *testing.T) { - iteratorDef := `"Iterator": {"StartAt": "SI0", "States": {"SI0": {"Type": "Pass", "End": true}}}` - stateMachineSource := `{"StartAt": "S0", "States": {"S0": {"Type": "Map" {0}, ` + iteratorDef + `, "End": true}}}` - expectedStateMachine := `{"StartAt": "S0", "States": {"S0": {"Type": "Map" , "Iterator": {"StartAt": "SI0", "States": {"SI0": {"Type": "Pass", "End": true}}}, "End": true}}}` - actualStateMachine := Format(stateMachineSource, "") - assert.Equal(t, expectedStateMachine, actualStateMachine) -} -func TestStrFormatWithIndexOutOfArgsRange(t *testing.T) { - template := "{3} - rings to the immortal elfs, {7} to dwarfs, {9} to greedy people and {1} to control everything" - expectedResult := "3 - rings to the immortal elfs, {7} to dwarfs, {9} to greedy people and 1 to control everything" - actualResult := Format(template, "0", "1", "2", "3") - assert.Equal(t, expectedResult, actualResult) -} - -func TestStrFormatComplexKeyNotFound(t *testing.T) { - template := "Hello: {username}, you earn {amount} $" - expectedResult := "Hello: {username}, you earn 1000 $" - actualResult := FormatComplex(template, map[string]interface{}{"amount": 1000}) - assert.Equal(t, expectedResult, actualResult) -} - -func TestStrFormatGeneric(t *testing.T) { - strFormat1 := "Here we are testing integers \"int8\": {0}, \"int16\": {1}, \"int32\": {2}, \"int64\": {3} and finally \"int\": {4}" - var v1 int8 = 8 - var v2 int16 = -16 - var v3 int32 = 32 - var v4 int64 = -64 - var v5 int = 123 - - strFormatResult := Format(strFormat1, v1, v2, v3, v4, v5) - assert.Equal(t, "Here we are testing integers \"int8\": 8, \"int16\": -16, \"int32\": 32, \"int64\": -64 and finally \"int\": 123", strFormatResult) - - strFormat2 := "Here we are testing integers \"uint8\": {0}, \"uint16\": {1}, \"uint32\": {2}, \"uint64\": {3} and finally \"uint\": {4}" - var v6 uint8 = 8 - var v7 uint16 = 16 - var v8 uint32 = 32 - var v9 uint64 = 64 - var v10 uint = 128 - - strFormatResult = Format(strFormat2, v6, v7, v8, v9, v10) - assert.Equal(t, "Here we are testing integers \"uint8\": 8, \"uint16\": 16, \"uint32\": 32, \"uint64\": 64 and finally \"uint\": 128", strFormatResult) + "github.com/stretchr/testify/assert" - strFormat3 := "Here we are testing floats \"float32\": {0}, \"float64\":{1}" - var v11 float32 = 1.24 - var v12 float64 = 1.56 - strFormatResult = Format(strFormat3, v11, v12) - assert.Equal(t, "Here we are testing floats \"float32\": 1.24, \"float64\":1.56", strFormatResult) + "github.com/wissance/stringFormatter" +) - strFormat4 := "Here we are testing \"bool\" args: {0}, {1}" - var v13 bool = false - var v14 bool = true - strFormatResult = Format(strFormat4, v13, v14) - assert.Equal(t, "Here we are testing \"bool\" args: false, true", strFormatResult) +const _address = "grpcs://127.0.0.1" - strFormat5 := "Here we are testing \"complex64\" {0} and \"complex128\": {1}" - var v15 complex64 = complex(1.0, 6.0) - var v16 complex128 = complex(2.3, 3.2) - strFormatResult = Format(strFormat5, v15, v16) - assert.Equal(t, "Here we are testing \"complex64\" (1+6i) and \"complex128\": (2.3+3.2i)", strFormatResult) +type Example struct { + Int int + Str string + Double float64 + Err error } -func TestStrFormatComplex(t *testing.T) { - strFormatResult := FormatComplex("Hello {user} what are you doing here {app} ?", map[string]interface{}{"user": "vpupkin", "app": "mn_console"}) - assert.Equal(t, "Hello vpupkin what are you doing here mn_console ?", strFormatResult) - - strFormatResult = FormatComplex("Current app settings are: ipAddr: {ipaddr}, port: {port}, use ssl: {ssl}.", map[string]interface{}{"ipaddr": "127.0.0.1", "port": 5432, "ssl": false}) - assert.Equal(t, "Current app settings are: ipAddr: 127.0.0.1, port: 5432, use ssl: false.", strFormatResult) +func TestFormat(t *testing.T) { + for name, test := range map[string]struct { + template string + args []any + expected string + }{ + "all args in place": { + template: "Hello i am {0}, my age is {1} and i am waiting for {2}, because i am {0}!", + args: []any{"Michael Ushakov (Evillord666)", "34", `"Great Success"`}, + expected: `Hello i am Michael Ushakov (Evillord666), my age is 34 and i am waiting for "Great Success", because i am Michael Ushakov (Evillord666)!`, + }, + "too large index": { + template: "We are wondering if these values would be replaced : {5}, {4}, {0}", + args: []any{"one", "two", "three"}, + expected: "We are wondering if these values would be replaced : {5}, {4}, one", + }, + "no args": { + template: "No args ... : {0}, {1}, {2}", + args: nil, + expected: "No args ... : {0}, {1}, {2}", + }, + "format json": { + template: ` + { + "Comment": "Call Lambda with GRPC", + "StartAt": "CallLambdaWithGrpc", + "States": {"CallLambdaWithGrpc": {"Type": "Task", "Resource": "{0}:get ad user", "End": true}} + }`, + args: []any{_address}, + expected: ` + { + "Comment": "Call Lambda with GRPC", + "StartAt": "CallLambdaWithGrpc", + "States": {"CallLambdaWithGrpc": {"Type": "Task", "Resource": "grpcs://127.0.0.1:get ad user", "End": true}} + }`, + }, + "multiple nested curly brackets": { + template: `{"StartAt": "S0", "States": {"S0": {"Type": "Map" {0}, ` + + `"Iterator": {"StartAt": "SI0", "States": {"SI0": {"Type": "Pass", "End": true}}}` + + `, "End": true}}}`, + args: []any{""}, + expected: `{"StartAt": "S0", "States": {"S0": {"Type": "Map" , "Iterator": {"StartAt": "SI0", "States": {"SI0": {"Type": "Pass", "End": true}}}, "End": true}}}`, + }, + "indexes out of args range": { + template: "{3} - rings to the immortal elfs, {7} to dwarfs, {9} to greedy people and {1} to control everything", + args: []any{"0", "1", "2", "3"}, + expected: "3 - rings to the immortal elfs, {7} to dwarfs, {9} to greedy people and 1 to control everything", + }, + "format integers": { + template: `Here we are testing integers "int8": {0}, "int16": {1}, "int32": {2}, "int64": {3} and finally "int": {4}`, + args: []any{int8(8), int16(-16), int32(32), int64(-64), int(123)}, + expected: `Here we are testing integers "int8": 8, "int16": -16, "int32": 32, "int64": -64 and finally "int": 123`, + }, + "format unsigneds": { + template: `Here we are testing integers "uint8": {0}, "uint16": {1}, "uint32": {2}, "uint64": {3} and finally "uint": {4}`, + args: []any{uint8(8), uint16(16), uint32(32), uint64(64), uint(128)}, + expected: `Here we are testing integers "uint8": 8, "uint16": 16, "uint32": 32, "uint64": 64 and finally "uint": 128`, + }, + "format floats": { + template: `Here we are testing floats "float32": {0}, "float64":{1}`, + args: []any{float32(1.24), float64(1.56)}, + expected: `Here we are testing floats "float32": 1.24, "float64":1.56`, + }, + "format bools": { + template: `Here we are testing "bool" args: {0}, {1}`, + args: []any{false, true}, + expected: `Here we are testing "bool" args: false, true`, + }, + "format complex": { + template: `Here we are testing "complex64" {0} and "complex128": {1}`, + args: []any{complex64(complex(1.0, 6.0)), complex(2.3, 3.2)}, + expected: `Here we are testing "complex64" (1+6i) and "complex128": (2.3+3.2i)`, + }, + "doubly curly brackets": { + template: "Hello i am {{0}}, my age is {1} and i am waiting for {2}, because i am {0}!", + args: []any{"Michael Ushakov (Evillord666)", "34", `"Great Success"`}, + expected: `Hello i am {0}, my age is 34 and i am waiting for "Great Success", because i am Michael Ushakov (Evillord666)!`, + }, + "doubly curly brackets at the end": { + template: "At the end {{0}}", + args: []any{"s"}, + expected: "At the end {0}", + }, + "struct arg": { + template: "Example is: {0}", + args: []any{ + Example{ + Int: 123, + Str: "This is a test str, nothing more special", + Double: -1.098743, + Err: errors.New("main question error, is 42"), + }, + }, + expected: "Example is: {123 This is a test str, nothing more special -1.098743 main question error, is 42}", + }, + } { + t.Run(name, func(t *testing.T) { + assert.Equal(t, test.expected, stringFormatter.Format(test.template, test.args...)) + }) + } } -func TestStrFormatStruct(t *testing.T) { - type Example struct { - Int int - Str string - Double float64 - Err error +// TestStrFormatWithComplicatedText - this test represents issue with complicated text +func TestFormatComplex(t *testing.T) { + for name, test := range map[string]struct { + template string + args map[string]any + expected string + }{ + "format json": { + template: ` + { + "Comment": "Call Lambda with GRPC", + "StartAt": "CallLambdaWithGrpc", + "States": {"CallLambdaWithGrpc": {"Type": "Task", "Resource": "{address}:get ad user", "End": true}} + }`, + args: map[string]any{"address": _address}, + expected: ` + { + "Comment": "Call Lambda with GRPC", + "StartAt": "CallLambdaWithGrpc", + "States": {"CallLambdaWithGrpc": {"Type": "Task", "Resource": "grpcs://127.0.0.1:get ad user", "End": true}} + }`, + }, + "key not found": { + template: "Hello: {username}, you earn {amount} $", + args: map[string]any{"amount": 1000}, + expected: "Hello: {username}, you earn 1000 $", + }, + "dialog": { + template: "Hello {user} what are you doing here {app} ?", + args: map[string]any{"user": "vpupkin", "app": "mn_console"}, + expected: "Hello vpupkin what are you doing here mn_console ?", + }, + "info message": { + template: "Current app settings are: ipAddr: {ipaddr}, port: {port}, use ssl: {ssl}.", + args: map[string]any{"ipaddr": "127.0.0.1", "port": 5432, "ssl": false}, + expected: "Current app settings are: ipAddr: 127.0.0.1, port: 5432, use ssl: false.", + }, + } { + t.Run(name, func(t *testing.T) { + assert.Equal(t, test.expected, stringFormatter.FormatComplex(test.template, test.args)) + }) } - - example1 := Example{Int: 123, Str: "This is a test str, nothing more special", Double: -1.098743, Err: errors.New("main question error, is 42")} - result := Format("Example is: {0}", example1) - assert.NotEmpty(t, result) - assert.Equal(t, "Example is: {123 This is a test str, nothing more special -1.098743 main question error, is 42}", result) } diff --git a/maptostring.go b/maptostring.go new file mode 100644 index 0000000..d16d665 --- /dev/null +++ b/maptostring.go @@ -0,0 +1,44 @@ +package stringFormatter + +import "strings" + +const ( + // KeyKey placeholder will be formatted to map key + KeyKey = "key" + // KeyValue placeholder will be formatted to map value + KeyValue = "value" +) + +// MapToString - format map keys and values according to format, joining parts with separator. +// Format should contain key and value placeholders which will be used for formatting, e.g. +// "{key} : {value}", or "{value}", or "{key} => {value}". +// Parts order in resulting string is not guranteed. +func MapToString[ + K string | int | uint | int32 | int64 | uint32 | uint64, + V any, +](data map[K]V, format string, separator string) string { + if len(data) == 0 { + return "" + } + + mapStr := &strings.Builder{} + // assuming format will be at most two times larger after formatting part, + // plus exact number of bytes for separators + mapStr.Grow(len(data)*len(format)*2 + (len(data)-1)*len(separator)) + + isFirst := true + for k, v := range data { + if !isFirst { + mapStr.WriteString(separator) + } + + line := FormatComplex(string(format), map[string]any{ + KeyKey: k, + KeyValue: v, + }) + mapStr.WriteString(line) + isFirst = false + } + + return mapStr.String() +} diff --git a/text_utils_benchmark_test.go b/maptostring_bench_test.go similarity index 72% rename from text_utils_benchmark_test.go rename to maptostring_bench_test.go index 7925a5e..5a6c742 100644 --- a/text_utils_benchmark_test.go +++ b/maptostring_bench_test.go @@ -1,9 +1,13 @@ -package stringFormatter +package stringFormatter_test -import "testing" +import ( + "testing" + + "github.com/wissance/stringFormatter" +) func BenchmarkMapToStringWith11Keys(b *testing.B) { - optionsMap := map[string]interface{}{ + optionsMap := map[string]any{ "timeoutMS": 2000, "connectTimeoutMS": 20000, "maxPoolSize": 64, @@ -18,6 +22,6 @@ func BenchmarkMapToStringWith11Keys(b *testing.B) { } for i := 0; i < b.N; i++ { - _ = MapToString(&optionsMap, KeyValueWithSemicolonSepFormat, ", ") + _ = stringFormatter.MapToString(optionsMap, "{key} : {value}", ", ") } } diff --git a/maptostring_test.go b/maptostring_test.go new file mode 100644 index 0000000..f2f844d --- /dev/null +++ b/maptostring_test.go @@ -0,0 +1,75 @@ +package stringFormatter_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/wissance/stringFormatter" +) + +const _separator = ", " + +func TestMapToString(t *testing.T) { + for name, test := range map[string]struct { + str string + expectedParts []string + }{ + "semicolon sep": { + str: stringFormatter.MapToString( + map[string]any{ + "connectTimeout": 1000, + "useSsl": true, + "login": "sa", + "password": "sa", + }, + "{key} : {value}", + _separator, + ), + expectedParts: []string{ + "connectTimeout : 1000", + "useSsl : true", + "login : sa", + "password : sa", + }, + }, + "arrow sep": { + str: stringFormatter.MapToString( + map[int]any{ + 1: "value 1", + 2: "value 2", + -5: "value -5", + }, + "{key} => {value}", + _separator, + ), + expectedParts: []string{ + "1 => value 1", + "2 => value 2", + "-5 => value -5", + }, + }, + "only value": { + str: stringFormatter.MapToString( + map[uint64]any{ + 1: "value 1", + 2: "value 2", + 5: "value 5", + }, + "{value}", + _separator, + ), + expectedParts: []string{ + "value 1", + "value 2", + "value 5", + }, + }, + } { + t.Run(name, func(t *testing.T) { + actualParts := strings.Split(test.str, _separator) + assert.ElementsMatch(t, test.expectedParts, actualParts) + }) + } +} diff --git a/text_utilities_test.go b/text_utilities_test.go deleted file mode 100644 index 5136249..0000000 --- a/text_utilities_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package stringFormatter - -import ( - "github.com/stretchr/testify/assert" - "strings" - "testing" -) - -func TestMapToString(t *testing.T) { - options := map[string]interface{}{ - "connectTimeout": 1000, - "useSsl": true, - "login": "sa", - "password": "sa", - } - - str := MapToString(&options, KeyValueWithSemicolonSepFormat, ", ") - assert.True(t, len(str) > 0) - // we check only parts because range produce string in random order - assert.True(t, strings.Contains(str, "connectTimeout : 1000")) - assert.True(t, strings.Contains(str, "useSsl : true")) - assert.True(t, strings.Contains(str, "login : sa")) - assert.True(t, strings.Contains(str, "password : sa")) - //assert.Equal(t, "connectTimeout : 1000, useSsl : true, login : sa, password : sa", str) - - anotherOptions := map[int]interface{}{ - 1: "value 1", - 2: "value 2", - -5: "value -5", - } - - str = MapToString(&anotherOptions, KeyValueWithArrowSepFormat, ", ") - assert.True(t, strings.Contains(str, "1 => value 1")) - assert.True(t, strings.Contains(str, "2 => value 2")) - assert.True(t, strings.Contains(str, "-5 => value -5")) - //assert.Equal(t, "1 => value 1, 2 => value 2, -5 => value -5", str) -} diff --git a/text_utils.go b/text_utils.go deleted file mode 100644 index c5afa0c..0000000 --- a/text_utils.go +++ /dev/null @@ -1,42 +0,0 @@ -package stringFormatter - -import "strings" - -type MapLineFormat string - -const ( - keyName = "key" - keyArg = "{" + keyName + "}" - valueName = "value" - valueArg = "{" + valueName + "}" -) - -const ( - KeyValueWithArrowSepFormat MapLineFormat = keyArg + " => " + valueArg - KeyValueWithSemicolonSepFormat MapLineFormat = keyArg + " : " + valueArg - ValueOnly MapLineFormat = valueArg -) - -func MapToString[TK string | int | uint | int32 | int64 | uint32 | uint64, TV any](data *map[TK]TV, format MapLineFormat, lineSeparator string) string { - - if data == nil || len(*data) == 0 { - return "" - } - var mapStr = &strings.Builder{} - empty := true - mapStr.Grow(len(*data) * 50) - lineData := map[string]interface{}{} - - for k, v := range *data { - lineData[keyName] = k - lineData[valueName] = v - line := FormatComplex(string(format), lineData) - // append - if !empty { - mapStr.WriteString(lineSeparator) - } - mapStr.WriteString(line) - empty = false - } - return mapStr.String() -} From 6b861813ff82d4a970153bb690d8151932930f8d Mon Sep 17 00:00:00 2001 From: Poletansky Viktor Date: Mon, 31 Jul 2023 17:34:41 +0300 Subject: [PATCH 2/2] dev: rename test struct --- formatter_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/formatter_test.go b/formatter_test.go index 8fa3c85..5c84577 100644 --- a/formatter_test.go +++ b/formatter_test.go @@ -11,7 +11,7 @@ import ( const _address = "grpcs://127.0.0.1" -type Example struct { +type meteoData struct { Int int Str string Double float64 @@ -104,7 +104,7 @@ func TestFormat(t *testing.T) { "struct arg": { template: "Example is: {0}", args: []any{ - Example{ + meteoData{ Int: 123, Str: "This is a test str, nothing more special", Double: -1.098743,