Skip to content

Commit 5c1924e

Browse files
committed
Finish v1.2.0
2 parents a32eee4 + 824854f commit 5c1924e

6 files changed

Lines changed: 294 additions & 29 deletions

File tree

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ faster than `fmt`!!!.
1414
## 1. Features
1515

1616
1. Text formatting with template using traditional for `C#, Python programmers style` - `{0}`, `{name}` that faster then fmt does:
17-
![String Formatter: a convenient string formatting tool](/img/benchmarks2.png)
17+
![String Formatter: a convenient string formatting tool](/img/benchmarks_adv.png)
1818
2. Additional text utilities:
1919
- convert ***map to string*** using one of predefined formats (see `text_utils.go`)
2020

@@ -58,7 +58,27 @@ strFormatResult = stringFormatter.FormatComplex(
5858
```
5959
a result will be: `"Current app settings are: ipAddr: 127.0.0.1, port: 5432, use ssl: false."``
6060

61-
#### 1.2.3 Benchmarks of the Format and FormatComplex functions
61+
##### 1.2.3 Advanced arguments formatting
62+
63+
For more convenient lines formatting we should choose how arguments are representing in output text,
64+
`stringFormatter` supports following format options:
65+
1. Bin number formatting
66+
- `{0:B}, 15 outputs -> 1111`
67+
- `{0:B8}, 15 outputs -> 00001111`
68+
2. Hex number formatting
69+
- `{0:X}, 250 outputs -> fa`
70+
- `{0:X4}, 250 outputs -> 00fa`
71+
3. Oct number formatting
72+
- `{0:o}, 11 outputs -> 14`
73+
4. Float point number formatting
74+
- `{0:E2}, 191.0478 outputs -> 1.91e+02`
75+
- `{0:F}, 10.4567890 outputs -> 10.456789`
76+
- `{0:F4}, 10.4567890 outputs -> 10.4568`
77+
- `{0:F8}, 10.4567890 outputs -> 10.45678900`
78+
5. Percentage output
79+
- `{0:P100}, 12 outputs -> 12%`
80+
81+
##### 1.2.4 Benchmarks of the Format and FormatComplex functions
6282

6383
benchmark could be running using following commands from command line:
6484
* to see `Format` result - `go test -bench=Format -benchmem -cpu 1`

formatter.go

Lines changed: 185 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"strings"
77
)
88

9+
const argumentFormatSeparator = ":"
10+
911
// Format
1012
/* Func that makes string formatting from template
1113
* It differs from above function only by generic interface that allow to use only primitive data types:
@@ -36,15 +38,16 @@ func Format(template string, args ...any) string {
3638
formattedStr.Grow(templateLen + 22*len(args))
3739
j := -1 //nolint:ineffassign
3840

41+
nestedBrackets := false
3942
formattedStr.WriteString(template[:start])
4043
for i := start; i < templateLen; i++ {
4144
if template[i] == '{' {
4245
// possibly it is a template placeholder
4346
if i == templateLen-1 {
4447
break
4548
}
46-
47-
if template[i+1] == '{' { // todo: umv: this not considering {{0}}
49+
// considering in 2 phases - {{ }}
50+
if template[i+1] == '{' {
4851
formattedStr.WriteByte('{')
4952
continue
5053
}
@@ -57,6 +60,7 @@ func Format(template string, args ...any) string {
5760

5861
if template[j] == '{' {
5962
// multiple nested curly brackets ...
63+
nestedBrackets = true
6064
formattedStr.WriteString(template[i:j])
6165
i = j
6266
}
@@ -74,18 +78,40 @@ func Format(template string, args ...any) string {
7478
i = j + 1
7579
} else {
7680
argNumberStr := template[i+1 : j]
81+
// is here we should support formatting ?
7782
var argNumber int
7883
var err error
84+
var argFormatOptions string
7985
if len(argNumberStr) == 1 {
80-
// this makes work a little faster than AtoI
86+
// this calculation makes work a little faster than AtoI
8187
argNumber = int(argNumberStr[0] - '0')
8288
} else {
83-
argNumber, err = strconv.Atoi(argNumberStr)
89+
argNumber = -1
90+
// Here we are going to process argument either with additional formatting or not
91+
// i.e. 0 for arg without formatting && 0:format for an argument wit formatting
92+
// todo(UMV): we could format json or yaml here ...
93+
formatOptionIndex := strings.Index(argNumberStr, argumentFormatSeparator)
94+
// formatOptionIndex can't be == 0, because 0 is a position of arg number
95+
if formatOptionIndex > 0 {
96+
// trim formatting string to remove spaces
97+
argFormatOptions = strings.Trim(argNumberStr[formatOptionIndex+1:], " ")
98+
argNumberStrPart := argNumberStr[:formatOptionIndex]
99+
argNumber, err = strconv.Atoi(strings.Trim(argNumberStrPart, " "))
100+
if err == nil {
101+
argNumberStr = argNumberStrPart
102+
}
103+
// make formatting option str for further pass to an argument
104+
}
105+
//
106+
if argNumber < 0 {
107+
argNumber, err = strconv.Atoi(argNumberStr)
108+
}
84109
}
85-
//argNumber, err := strconv.Atoi(argNumberStr)
86-
if err == nil && len(args) > argNumber {
110+
111+
if (err == nil || (argFormatOptions != "" && !nestedBrackets)) &&
112+
len(args) > argNumber {
87113
// get number from placeholder
88-
strVal := getItemAsStr(&args[argNumber])
114+
strVal := getItemAsStr(&args[argNumber], &argFormatOptions)
89115
formattedStr.WriteString(strVal)
90116
} else {
91117
formattedStr.WriteByte('{')
@@ -124,6 +150,7 @@ func FormatComplex(template string, args map[string]any) string {
124150
formattedStr := &strings.Builder{}
125151
formattedStr.Grow(templateLen + 22*len(args))
126152
j := -1 //nolint:ineffassign
153+
nestedBrackets := false
127154
formattedStr.WriteString(template[:start])
128155
for i := start; i < templateLen; i++ {
129156
if template[i] == '{' {
@@ -145,6 +172,7 @@ func FormatComplex(template string, args map[string]any) string {
145172
}
146173
if template[j] == '{' {
147174
// multiple nested curly brackets ...
175+
nestedBrackets = true
148176
formattedStr.WriteString(template[i:j])
149177
i = j
150178
}
@@ -159,11 +187,21 @@ func FormatComplex(template string, args map[string]any) string {
159187
formattedStr.WriteString(template[i+1 : j+1])
160188
i = j + 1
161189
} else {
190+
var argFormatOptions string
162191
argNumberStr := template[i+1 : j]
163192
arg, ok := args[argNumberStr]
164-
if ok {
193+
if !ok {
194+
formatOptionIndex := strings.Index(argNumberStr, argumentFormatSeparator)
195+
if formatOptionIndex >= 0 {
196+
argFormatOptions = strings.Trim(argNumberStr[formatOptionIndex+1:], " ")
197+
argNumberStr = strings.Trim(argNumberStr[:formatOptionIndex], " ")
198+
}
199+
200+
arg, ok = args[argNumberStr]
201+
}
202+
if ok || (argFormatOptions != "" && !nestedBrackets) {
165203
// get number from placeholder
166-
strVal := getItemAsStr(&arg)
204+
strVal := getItemAsStr(&arg, &argFormatOptions)
167205
formattedStr.WriteString(strVal)
168206
} else {
169207
formattedStr.WriteByte('{')
@@ -181,38 +219,159 @@ func FormatComplex(template string, args map[string]any) string {
181219
return formattedStr.String()
182220
}
183221

184-
// todo: umv: impl format passing as param
185-
func getItemAsStr(item *any) string {
222+
func getItemAsStr(item *any, itemFormat *string) string {
223+
base := 10
224+
var floatFormat byte = 'f'
225+
precision := -1
226+
var preparedArgFormat string
227+
var argStr string
228+
postProcessingRequired := false
229+
intNumberFormat := false
230+
floatNumberFormat := false
231+
232+
if itemFormat != nil && len(*itemFormat) > 0 {
233+
/* for numbers there are following formats:
234+
* d(D) - decimal
235+
* b(B) - binary
236+
* f(F) - fixed point i.e {0:F}, 10.5467890 -> 10.546789 ; {0:F4}, 10.5467890 -> 10.5468
237+
* e(E) - exponential - float point with scientific format {0:E2}, 191.0784 -> 1.91e+02
238+
* x(X) - hexadecimal i.e. {0:X}, 250 -> fa ; {0:X4}, 250 -> 00fa
239+
* p(P) - percent i.e. {0:P100}, 12 -> 12%
240+
* Following formats are not supported yet:
241+
* 1. c(C) currency it requires also country code
242+
* 2. g(G),and others with locales
243+
* 3. f(F) - fixed point, {0,F4}, 123.15 -> 123.1500
244+
* OUR own addition:
245+
* 1. O(o) - octahedral number format
246+
*/
247+
preparedArgFormat = *itemFormat
248+
postProcessingRequired = len(preparedArgFormat) > 1
249+
250+
switch rune((*itemFormat)[0]) {
251+
case 'd', 'D':
252+
base = 10
253+
intNumberFormat = true
254+
case 'x', 'X':
255+
base = 16
256+
intNumberFormat = true
257+
case 'o', 'O':
258+
base = 8
259+
intNumberFormat = true
260+
case 'b', 'B':
261+
base = 2
262+
intNumberFormat = true
263+
case 'e', 'E', 'f', 'F':
264+
if rune(preparedArgFormat[0]) == 'e' || rune(preparedArgFormat[0]) == 'E' {
265+
floatFormat = 'e'
266+
}
267+
// precision was passed, take [1:end], extract precision
268+
if postProcessingRequired {
269+
precisionStr := preparedArgFormat[1:]
270+
precisionVal, err := strconv.Atoi(precisionStr)
271+
if err == nil {
272+
precision = precisionVal
273+
}
274+
}
275+
postProcessingRequired = false
276+
floatNumberFormat = floatFormat == 'f'
277+
278+
case 'p', 'P':
279+
// percentage processes here ...
280+
if postProcessingRequired {
281+
dividerStr := preparedArgFormat[1:]
282+
dividerVal, err := strconv.ParseFloat(dividerStr, 32)
283+
if err == nil {
284+
// 1. Convert arg to float
285+
val := (*item).(interface{})
286+
var floatVal float64
287+
switch val.(type) {
288+
case float64:
289+
floatVal = val.(float64)
290+
case int:
291+
floatVal = float64(val.(int))
292+
default:
293+
floatVal = 0
294+
}
295+
// 2. Divide arg / divider and multiply by 100
296+
percentage := (floatVal / dividerVal) * 100
297+
return strconv.FormatFloat(percentage, floatFormat, 2, 64)
298+
}
299+
}
300+
301+
default:
302+
base = 10
303+
}
304+
}
305+
186306
switch v := (*item).(type) {
187307
case string:
188-
return v
308+
argStr = v
189309
case int8:
190-
return strconv.FormatInt(int64(v), 10)
310+
argStr = strconv.FormatInt(int64(v), base)
191311
case int16:
192-
return strconv.FormatInt(int64(v), 10)
312+
argStr = strconv.FormatInt(int64(v), base)
193313
case int32:
194-
return strconv.FormatInt(int64(v), 10)
314+
argStr = strconv.FormatInt(int64(v), base)
195315
case int64:
196-
return strconv.FormatInt(v, 10)
316+
argStr = strconv.FormatInt(v, base)
197317
case int:
198-
return strconv.FormatInt(int64(v), 10)
318+
argStr = strconv.FormatInt(int64(v), base)
199319
case uint8:
200-
return strconv.FormatUint(uint64(v), 10)
320+
argStr = strconv.FormatUint(uint64(v), base)
201321
case uint16:
202-
return strconv.FormatUint(uint64(v), 10)
322+
argStr = strconv.FormatUint(uint64(v), base)
203323
case uint32:
204-
return strconv.FormatUint(uint64(v), 10)
324+
argStr = strconv.FormatUint(uint64(v), base)
205325
case uint64:
206-
return strconv.FormatUint(v, 10)
326+
argStr = strconv.FormatUint(v, base)
207327
case uint:
208-
return strconv.FormatUint(uint64(v), 10)
328+
argStr = strconv.FormatUint(uint64(v), base)
209329
case bool:
210-
return strconv.FormatBool(v)
330+
argStr = strconv.FormatBool(v)
211331
case float32:
212-
return strconv.FormatFloat(float64(v), 'f', -1, 32)
332+
argStr = strconv.FormatFloat(float64(v), floatFormat, precision, 32)
213333
case float64:
214-
return strconv.FormatFloat(v, 'f', -1, 64)
334+
argStr = strconv.FormatFloat(v, floatFormat, precision, 64)
215335
default:
216-
return fmt.Sprintf("%v", v)
336+
argStr = fmt.Sprintf("%v", v)
337+
}
338+
339+
if !postProcessingRequired {
340+
return argStr
217341
}
342+
343+
// 1. If integer numbers add filling
344+
if intNumberFormat {
345+
symbolsStr := preparedArgFormat[1:]
346+
symbolsStrVal, err := strconv.Atoi(symbolsStr)
347+
if err == nil {
348+
symbolsToAdd := symbolsStrVal - len(argStr)
349+
if symbolsToAdd > 0 {
350+
advArgStr := strings.Builder{}
351+
advArgStr.Grow(len(argStr) + symbolsToAdd + 1)
352+
353+
for i := 0; i < symbolsToAdd; i++ {
354+
advArgStr.WriteByte('0')
355+
}
356+
advArgStr.WriteString(argStr)
357+
return advArgStr.String()
358+
}
359+
}
360+
}
361+
362+
if floatNumberFormat && precision > 0 {
363+
pointIndex := strings.Index(argStr, ".")
364+
if pointIndex > 0 {
365+
advArgStr := strings.Builder{}
366+
advArgStr.Grow(len(argStr) + precision + 1)
367+
advArgStr.WriteString(argStr)
368+
numberOfSymbolsAfterPoint := len(argStr) - (pointIndex + 1)
369+
for i := numberOfSymbolsAfterPoint; i < precision; i++ {
370+
advArgStr.WriteByte(0)
371+
}
372+
return advArgStr.String()
373+
}
374+
}
375+
376+
return argStr
218377
}

formatter_benchmark_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ func BenchmarkFormat4Arg(b *testing.B) {
1515
}
1616
}
1717

18+
func BenchmarkFormat4ArgAdvanced(b *testing.B) {
19+
for i := 0; i < b.N; i++ {
20+
_ = Format(
21+
"Today is : {0}, atmosphere pressure is : {1:E2} mmHg, temperature: {2:E3}, location: {3}",
22+
time.Now().String(), 725, -15.54, "Yekaterinburg",
23+
)
24+
}
25+
}
26+
1827
func BenchmarkFmt4Arg(b *testing.B) {
1928
for i := 0; i < b.N; i++ {
2029
_ = fmt.Sprintf(
@@ -24,6 +33,15 @@ func BenchmarkFmt4Arg(b *testing.B) {
2433
}
2534
}
2635

36+
func BenchmarkFmt4ArgAdvanced(b *testing.B) {
37+
for i := 0; i < b.N; i++ {
38+
_ = fmt.Sprintf(
39+
"Today is : %s, atmosphere pressure is : %.3e mmHg, temperature: %.2f, location: %s",
40+
time.Now().String(), 725.0, -15.54, "Yekaterinburg",
41+
)
42+
}
43+
}
44+
2745
func BenchmarkFormat6Arg(b *testing.B) {
2846
for i := 0; i < b.N; i++ {
2947
_ = Format(

0 commit comments

Comments
 (0)