Skip to content

Commit

Permalink
Finish v1.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
EvilLord666 committed Jan 2, 2024
2 parents a32eee4 + 824854f commit 5c1924e
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 29 deletions.
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ faster than `fmt`!!!.
## 1. Features

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

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

#### 1.2.3 Benchmarks of the Format and FormatComplex functions
##### 1.2.3 Advanced arguments formatting

For more convenient lines formatting we should choose how arguments are representing in output text,
`stringFormatter` supports following format options:
1. Bin number formatting
- `{0:B}, 15 outputs -> 1111`
- `{0:B8}, 15 outputs -> 00001111`
2. Hex number formatting
- `{0:X}, 250 outputs -> fa`
- `{0:X4}, 250 outputs -> 00fa`
3. Oct number formatting
- `{0:o}, 11 outputs -> 14`
4. Float point number formatting
- `{0:E2}, 191.0478 outputs -> 1.91e+02`
- `{0:F}, 10.4567890 outputs -> 10.456789`
- `{0:F4}, 10.4567890 outputs -> 10.4568`
- `{0:F8}, 10.4567890 outputs -> 10.45678900`
5. Percentage output
- `{0:P100}, 12 outputs -> 12%`

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

benchmark could be running using following commands from command line:
* to see `Format` result - `go test -bench=Format -benchmem -cpu 1`
Expand Down
211 changes: 185 additions & 26 deletions formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"strings"
)

const argumentFormatSeparator = ":"

// Format
/* Func that makes string formatting from template
* It differs from above function only by generic interface that allow to use only primitive data types:
Expand Down Expand Up @@ -36,15 +38,16 @@ func Format(template string, args ...any) string {
formattedStr.Grow(templateLen + 22*len(args))
j := -1 //nolint:ineffassign

nestedBrackets := false
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}}
// considering in 2 phases - {{ }}
if template[i+1] == '{' {
formattedStr.WriteByte('{')
continue
}
Expand All @@ -57,6 +60,7 @@ func Format(template string, args ...any) string {

if template[j] == '{' {
// multiple nested curly brackets ...
nestedBrackets = true
formattedStr.WriteString(template[i:j])
i = j
}
Expand All @@ -74,18 +78,40 @@ func Format(template string, args ...any) string {
i = j + 1
} else {
argNumberStr := template[i+1 : j]
// is here we should support formatting ?
var argNumber int
var err error
var argFormatOptions string
if len(argNumberStr) == 1 {
// this makes work a little faster than AtoI
// this calculation makes work a little faster than AtoI
argNumber = int(argNumberStr[0] - '0')
} else {
argNumber, err = strconv.Atoi(argNumberStr)
argNumber = -1
// Here we are going to process argument either with additional formatting or not
// i.e. 0 for arg without formatting && 0:format for an argument wit formatting
// todo(UMV): we could format json or yaml here ...
formatOptionIndex := strings.Index(argNumberStr, argumentFormatSeparator)
// formatOptionIndex can't be == 0, because 0 is a position of arg number
if formatOptionIndex > 0 {
// trim formatting string to remove spaces
argFormatOptions = strings.Trim(argNumberStr[formatOptionIndex+1:], " ")
argNumberStrPart := argNumberStr[:formatOptionIndex]
argNumber, err = strconv.Atoi(strings.Trim(argNumberStrPart, " "))
if err == nil {
argNumberStr = argNumberStrPart
}
// make formatting option str for further pass to an argument
}
//
if argNumber < 0 {
argNumber, err = strconv.Atoi(argNumberStr)
}
}
//argNumber, err := strconv.Atoi(argNumberStr)
if err == nil && len(args) > argNumber {

if (err == nil || (argFormatOptions != "" && !nestedBrackets)) &&
len(args) > argNumber {
// get number from placeholder
strVal := getItemAsStr(&args[argNumber])
strVal := getItemAsStr(&args[argNumber], &argFormatOptions)
formattedStr.WriteString(strVal)
} else {
formattedStr.WriteByte('{')
Expand Down Expand Up @@ -124,6 +150,7 @@ func FormatComplex(template string, args map[string]any) string {
formattedStr := &strings.Builder{}
formattedStr.Grow(templateLen + 22*len(args))
j := -1 //nolint:ineffassign
nestedBrackets := false
formattedStr.WriteString(template[:start])
for i := start; i < templateLen; i++ {
if template[i] == '{' {
Expand All @@ -145,6 +172,7 @@ func FormatComplex(template string, args map[string]any) string {
}
if template[j] == '{' {
// multiple nested curly brackets ...
nestedBrackets = true
formattedStr.WriteString(template[i:j])
i = j
}
Expand All @@ -159,11 +187,21 @@ func FormatComplex(template string, args map[string]any) string {
formattedStr.WriteString(template[i+1 : j+1])
i = j + 1
} else {
var argFormatOptions string
argNumberStr := template[i+1 : j]
arg, ok := args[argNumberStr]
if ok {
if !ok {
formatOptionIndex := strings.Index(argNumberStr, argumentFormatSeparator)
if formatOptionIndex >= 0 {
argFormatOptions = strings.Trim(argNumberStr[formatOptionIndex+1:], " ")
argNumberStr = strings.Trim(argNumberStr[:formatOptionIndex], " ")
}

arg, ok = args[argNumberStr]
}
if ok || (argFormatOptions != "" && !nestedBrackets) {
// get number from placeholder
strVal := getItemAsStr(&arg)
strVal := getItemAsStr(&arg, &argFormatOptions)
formattedStr.WriteString(strVal)
} else {
formattedStr.WriteByte('{')
Expand All @@ -181,38 +219,159 @@ func FormatComplex(template string, args map[string]any) string {
return formattedStr.String()
}

// todo: umv: impl format passing as param
func getItemAsStr(item *any) string {
func getItemAsStr(item *any, itemFormat *string) string {
base := 10
var floatFormat byte = 'f'
precision := -1
var preparedArgFormat string
var argStr string
postProcessingRequired := false
intNumberFormat := false
floatNumberFormat := false

if itemFormat != nil && len(*itemFormat) > 0 {
/* for numbers there are following formats:
* d(D) - decimal
* b(B) - binary
* f(F) - fixed point i.e {0:F}, 10.5467890 -> 10.546789 ; {0:F4}, 10.5467890 -> 10.5468
* e(E) - exponential - float point with scientific format {0:E2}, 191.0784 -> 1.91e+02
* x(X) - hexadecimal i.e. {0:X}, 250 -> fa ; {0:X4}, 250 -> 00fa
* p(P) - percent i.e. {0:P100}, 12 -> 12%
* Following formats are not supported yet:
* 1. c(C) currency it requires also country code
* 2. g(G),and others with locales
* 3. f(F) - fixed point, {0,F4}, 123.15 -> 123.1500
* OUR own addition:
* 1. O(o) - octahedral number format
*/
preparedArgFormat = *itemFormat
postProcessingRequired = len(preparedArgFormat) > 1

switch rune((*itemFormat)[0]) {
case 'd', 'D':
base = 10
intNumberFormat = true
case 'x', 'X':
base = 16
intNumberFormat = true
case 'o', 'O':
base = 8
intNumberFormat = true
case 'b', 'B':
base = 2
intNumberFormat = true
case 'e', 'E', 'f', 'F':
if rune(preparedArgFormat[0]) == 'e' || rune(preparedArgFormat[0]) == 'E' {
floatFormat = 'e'
}
// precision was passed, take [1:end], extract precision
if postProcessingRequired {
precisionStr := preparedArgFormat[1:]
precisionVal, err := strconv.Atoi(precisionStr)
if err == nil {
precision = precisionVal
}
}
postProcessingRequired = false
floatNumberFormat = floatFormat == 'f'

case 'p', 'P':
// percentage processes here ...
if postProcessingRequired {
dividerStr := preparedArgFormat[1:]
dividerVal, err := strconv.ParseFloat(dividerStr, 32)
if err == nil {
// 1. Convert arg to float
val := (*item).(interface{})
var floatVal float64
switch val.(type) {
case float64:
floatVal = val.(float64)
case int:
floatVal = float64(val.(int))
default:
floatVal = 0
}
// 2. Divide arg / divider and multiply by 100
percentage := (floatVal / dividerVal) * 100
return strconv.FormatFloat(percentage, floatFormat, 2, 64)
}
}

default:
base = 10
}
}

switch v := (*item).(type) {
case string:
return v
argStr = v
case int8:
return strconv.FormatInt(int64(v), 10)
argStr = strconv.FormatInt(int64(v), base)
case int16:
return strconv.FormatInt(int64(v), 10)
argStr = strconv.FormatInt(int64(v), base)
case int32:
return strconv.FormatInt(int64(v), 10)
argStr = strconv.FormatInt(int64(v), base)
case int64:
return strconv.FormatInt(v, 10)
argStr = strconv.FormatInt(v, base)
case int:
return strconv.FormatInt(int64(v), 10)
argStr = strconv.FormatInt(int64(v), base)
case uint8:
return strconv.FormatUint(uint64(v), 10)
argStr = strconv.FormatUint(uint64(v), base)
case uint16:
return strconv.FormatUint(uint64(v), 10)
argStr = strconv.FormatUint(uint64(v), base)
case uint32:
return strconv.FormatUint(uint64(v), 10)
argStr = strconv.FormatUint(uint64(v), base)
case uint64:
return strconv.FormatUint(v, 10)
argStr = strconv.FormatUint(v, base)
case uint:
return strconv.FormatUint(uint64(v), 10)
argStr = strconv.FormatUint(uint64(v), base)
case bool:
return strconv.FormatBool(v)
argStr = strconv.FormatBool(v)
case float32:
return strconv.FormatFloat(float64(v), 'f', -1, 32)
argStr = strconv.FormatFloat(float64(v), floatFormat, precision, 32)
case float64:
return strconv.FormatFloat(v, 'f', -1, 64)
argStr = strconv.FormatFloat(v, floatFormat, precision, 64)
default:
return fmt.Sprintf("%v", v)
argStr = fmt.Sprintf("%v", v)
}

if !postProcessingRequired {
return argStr
}

// 1. If integer numbers add filling
if intNumberFormat {
symbolsStr := preparedArgFormat[1:]
symbolsStrVal, err := strconv.Atoi(symbolsStr)
if err == nil {
symbolsToAdd := symbolsStrVal - len(argStr)
if symbolsToAdd > 0 {
advArgStr := strings.Builder{}
advArgStr.Grow(len(argStr) + symbolsToAdd + 1)

for i := 0; i < symbolsToAdd; i++ {
advArgStr.WriteByte('0')
}
advArgStr.WriteString(argStr)
return advArgStr.String()
}
}
}

if floatNumberFormat && precision > 0 {
pointIndex := strings.Index(argStr, ".")
if pointIndex > 0 {
advArgStr := strings.Builder{}
advArgStr.Grow(len(argStr) + precision + 1)
advArgStr.WriteString(argStr)
numberOfSymbolsAfterPoint := len(argStr) - (pointIndex + 1)
for i := numberOfSymbolsAfterPoint; i < precision; i++ {
advArgStr.WriteByte(0)
}
return advArgStr.String()
}
}

return argStr
}
18 changes: 18 additions & 0 deletions formatter_benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ func BenchmarkFormat4Arg(b *testing.B) {
}
}

func BenchmarkFormat4ArgAdvanced(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Format(
"Today is : {0}, atmosphere pressure is : {1:E2} mmHg, temperature: {2:E3}, location: {3}",
time.Now().String(), 725, -15.54, "Yekaterinburg",
)
}
}

func BenchmarkFmt4Arg(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf(
Expand All @@ -24,6 +33,15 @@ func BenchmarkFmt4Arg(b *testing.B) {
}
}

func BenchmarkFmt4ArgAdvanced(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf(
"Today is : %s, atmosphere pressure is : %.3e mmHg, temperature: %.2f, location: %s",
time.Now().String(), 725.0, -15.54, "Yekaterinburg",
)
}
}

func BenchmarkFormat6Arg(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Format(
Expand Down
Loading

0 comments on commit 5c1924e

Please sign in to comment.