Skip to content

Commit c6e1db8

Browse files
committed
Merge branch 'release/v1.4.0'
2 parents b6cd0fb + 6d6358d commit c6e1db8

10 files changed

+188
-15
lines changed

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# StringFormatter
22

33
A set of a ***high performance string tools*** that helps to build strings from templates and process text faster than with `fmt`!!!.
4+
Slice printing is **50% faster with 8 items** slice and **250% with 20 items** slice
45

56
![GitHub go.mod Go version (subdirectory of monorepo)](https://img.shields.io/github/go-mod/go-version/wissance/stringFormatter?style=plastic)
67
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/wissance/stringFormatter?style=plastic)
@@ -76,6 +77,9 @@ For more convenient lines formatting we should choose how arguments are represen
7677
- `{0:F8}, 10.4567890 outputs -> 10.45678900`
7778
5. Percentage output
7879
- `{0:P100}, 12 outputs -> 12%`
80+
6. Lists
81+
- `{0:L-}, [1,2,3] outputs -> 1-2-3`
82+
- `{0:L, }, [1,2,3] outputs -> 1, 2, 3`
7983

8084
##### 1.2.4 Benchmarks of the Format and FormatComplex functions
8185

@@ -107,12 +111,26 @@ str := stringFormatter.MapToString(&options, "{key} : {value}", ", ")
107111
"connectTimeout : 1000, useSsl : true, login : sa, password : sa"
108112
```
109113

110-
#### 2.2 Benchmarks of the MapToStr function
114+
#### 2.2 Benchmarks of the MapToString function
111115

112116
* to see `MapToStr` result - `go test -bench=MapToStr -benchmem -cpu 1`
113117

114118
![MapToStr benchmarks](/img/map2str_benchmarks.png)
115119

120+
#### 2.3 Slice to string utility
121+
122+
`SliceToString` - function that converts slice with passed separation between items to string.
123+
```go
124+
slice := []any{100, "200", 300, "400", 500, 600, "700", 800, 1.09, "hello"}
125+
separator := ","
126+
result := stringFormatter.SliceToString(&slice, &separator)
127+
```
128+
129+
#### 2.4 Benchmarks of the SliceToString function
130+
131+
`sf` is rather fast then `fmt` 2.5 times (250%) faster on slice with 20 items, see benchmark:
132+
![SliceToStr benchmarks](/img/slice2str_benchmarks.png)
133+
116134
### 3. Contributors
117135

118136
<a href="https://github.com/Wissance/stringFormatter/graphs/contributors">

formatter.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
)
88

99
const argumentFormatSeparator = ":"
10-
const bytesPerArgDefault = 20
10+
const bytesPerArgDefault = 16
1111

1212
// Format
1313
/* Func that makes string formatting from template
@@ -105,8 +105,8 @@ func Format(template string, args ...any) string {
105105
formatOptionIndex := strings.Index(argNumberStr, argumentFormatSeparator)
106106
// formatOptionIndex can't be == 0, because 0 is a position of arg number
107107
if formatOptionIndex > 0 {
108-
// trim formatting string to remove spaces
109-
argFormatOptions = strings.Trim(argNumberStr[formatOptionIndex+1:], " ")
108+
// trimmed was down later due to we could format list with space separator
109+
argFormatOptions = argNumberStr[formatOptionIndex+1:]
110110
argNumberStrPart := argNumberStr[:formatOptionIndex]
111111
argNumber, err = strconv.Atoi(strings.Trim(argNumberStrPart, " "))
112112
if err == nil {
@@ -218,7 +218,8 @@ func FormatComplex(template string, args map[string]any) string {
218218
if !ok {
219219
formatOptionIndex := strings.Index(argNumberStr, argumentFormatSeparator)
220220
if formatOptionIndex >= 0 {
221-
argFormatOptions = strings.Trim(argNumberStr[formatOptionIndex+1:], " ")
221+
// argFormatOptions = strings.Trim(argNumberStr[formatOptionIndex+1:], " ")
222+
argFormatOptions = argNumberStr[formatOptionIndex+1:]
222223
argNumberStr = strings.Trim(argNumberStr[:formatOptionIndex], " ")
223224
}
224225

@@ -278,10 +279,11 @@ func getItemAsStr(item *any, itemFormat *string) string {
278279
* OUR own addition:
279280
* 1. O(o) - octahedral number format
280281
*/
281-
preparedArgFormat = *itemFormat
282+
// preparedArgFormat is trimmed format, L type could contain spaces
283+
preparedArgFormat = strings.Trim(*itemFormat, " ")
282284
postProcessingRequired = len(preparedArgFormat) > 1
283285

284-
switch rune((*itemFormat)[0]) {
286+
switch rune(preparedArgFormat[0]) {
285287
case 'd', 'D':
286288
base = 10
287289
intNumberFormat = true
@@ -331,7 +333,15 @@ func getItemAsStr(item *any, itemFormat *string) string {
331333
return strconv.FormatFloat(percentage, floatFormat, 2, 64)
332334
}
333335
}
334-
336+
// l(L) is for list(slice)
337+
case 'l', 'L':
338+
separator := ","
339+
if len(*itemFormat) > 1 {
340+
separator = (*itemFormat)[1:]
341+
}
342+
// slice processing converting to {item}{delimiter}{item}{delimiter}{item}
343+
slice := (*item).([]any)
344+
return SliceToString(&slice, &separator)
335345
default:
336346
base = 10
337347
}

formatter_benchmark_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
package stringFormatter
1+
package stringFormatter_test
22

33
import (
44
"fmt"
5+
"github.com/wissance/stringFormatter"
56
"testing"
67
"time"
78
)
89

910
func BenchmarkFormat4Arg(b *testing.B) {
1011
for i := 0; i < b.N; i++ {
11-
_ = Format(
12+
_ = stringFormatter.Format(
1213
"Today is : {0}, atmosphere pressure is : {1} mmHg, temperature: {2}, location: {3}",
1314
time.Now().String(), 725, -1.54, "Yekaterinburg",
1415
)
@@ -17,7 +18,7 @@ func BenchmarkFormat4Arg(b *testing.B) {
1718

1819
func BenchmarkFormat4ArgAdvanced(b *testing.B) {
1920
for i := 0; i < b.N; i++ {
20-
_ = Format(
21+
_ = stringFormatter.Format(
2122
"Today is : {0}, atmosphere pressure is : {1:E2} mmHg, temperature: {2:E3}, location: {3}",
2223
time.Now().String(), 725, -15.54, "Yekaterinburg",
2324
)
@@ -44,7 +45,7 @@ func BenchmarkFmt4ArgAdvanced(b *testing.B) {
4445

4546
func BenchmarkFormat6Arg(b *testing.B) {
4647
for i := 0; i < b.N; i++ {
47-
_ = Format(
48+
_ = stringFormatter.Format(
4849
"Today is : {0}, atmosphere pressure is : {1} mmHg, temperature: {2}, location: {3}, coord:{4}-{5}",
4950
time.Now().String(), 725, -1.54, "Yekaterinburg", "64.245", "37.895",
5051
)
@@ -71,7 +72,7 @@ func BenchmarkFormatComplex7Arg(b *testing.B) {
7172
"latitude": "35.489",
7273
}
7374
for i := 0; i < b.N; i++ {
74-
_ = FormatComplex(
75+
_ = stringFormatter.FormatComplex(
7576
"Today is : {time}, atmosphere pressure is : {pressure} mmHg, humidity: {humidity}, temperature: {temperature}, location: {location}, coords:{longitude}-{latitude}",
7677
args,
7778
)

formatter_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,21 @@ func TestFormatWithArgFormatting(t *testing.T) {
183183
args: []any{12, "ass"},
184184
expected: "This is the text with percentage format - 12.00 / 11.94, and non normal percentage 0.00",
185185
},
186+
"list_with_default_sep": {
187+
template: "This is a list(slice) test: {0:L}",
188+
args: []any{[]any{"s1", "s2", "s3"}},
189+
expected: "This is a list(slice) test: s1,s2,s3",
190+
},
191+
"list_with_dash_sep": {
192+
template: "This is a list(slice) test: {0:L-}",
193+
args: []any{[]any{"s1", "s2", "s3"}},
194+
expected: "This is a list(slice) test: s1-s2-s3",
195+
},
196+
"list_with_space_sep": {
197+
template: "This is a list(slice) test: {0:L }",
198+
args: []any{[]any{"s1", "s2", "s3"}},
199+
expected: "This is a list(slice) test: s1 s2 s3",
200+
},
186201
} {
187202
// Run test here
188203
t.Run(name, func(t *testing.T) {
@@ -284,6 +299,21 @@ func TestFormatComplexWithArgFormatting(t *testing.T) {
284299
args: map[string]any{"float": 10.5467890},
285300
expected: "This is the text with an only number formatting: decimal - 10.546789 / 10.5468 / 10.54678900",
286301
},
302+
"list_with_default_sep": {
303+
template: "This is a list(slice) test: {list:L}",
304+
args: map[string]any{"list": []any{"s1", "s2", "s3"}},
305+
expected: "This is a list(slice) test: s1,s2,s3",
306+
},
307+
"list_with_dash_sep": {
308+
template: "This is a list(slice) test: {list:L-}",
309+
args: map[string]any{"list": []any{"s1", "s2", "s3"}},
310+
expected: "This is a list(slice) test: s1-s2-s3",
311+
},
312+
"list_with_space_sep": {
313+
template: "This is a list(slice) test: {list:L }",
314+
args: map[string]any{"list": []any{"s1", "s2", "s3"}},
315+
expected: "This is a list(slice) test: s1 s2 s3",
316+
},
287317
} {
288318
t.Run(name, func(t *testing.T) {
289319
assert.Equal(t, test.expected, stringFormatter.FormatComplex(test.template, test.args))

img/slice2str_benchmarks.png

151 KB
Loading

slicetostring.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
}

slicetostring_benchmark_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 BenchmarkSliceStandard10MixedItems(b *testing.B) {
33+
slice := []any{100, "200", 300, "400", 500, 600, "700", 800, 1.09, "hello"}
34+
for i := 0; i < b.N; i++ {
35+
_ = fmt.Sprintf("%+q", slice)
36+
}
37+
}
38+
39+
func BenchmarkSliceToStringAdvanced20StrItems(b *testing.B) {
40+
slice := []any{"str1", "str2", "str3", "str4", "str5", "str6", "str7", "str8", "str9", "str10",
41+
"str11", "str12", "str13", "str14", "str15", "str16", "str17", "str18", "str19", "str20"}
42+
separator := ","
43+
for i := 0; i < b.N; i++ {
44+
_ = stringFormatter.SliceToString(&slice, &separator)
45+
}
46+
}
47+
48+
func BenchmarkSliceStandard20StrItems(b *testing.B) {
49+
slice := []any{"str1", "str2", "str3", "str4", "str5", "str6", "str7", "str8", "str9", "str10",
50+
"str11", "str12", "str13", "str14", "str15", "str16", "str17", "str18", "str19", "str20"}
51+
for i := 0; i < b.N; i++ {
52+
_ = fmt.Sprintf("%+q", slice)
53+
}
54+
}

slicetostring_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
}

utils/run_benchamrks.ps1

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
$root_dir = Resolve-Path -Path ".."
2-
echo "******** 1. standadrd fmt formatting lib benchmarks ******** "
2+
echo "******** 1. standard fmt formatting lib benchmarks ******** "
33
go test $root_dir -bench=Fmt -benchmem -cpu 1
44
echo "******** 2. stringFormatter lib benchmarks ******** "
5-
go test $root_dir -bench=Format -benchmem -cpu 1
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

0 commit comments

Comments
 (0)