├── .github
└── ISSUE_TEMPLATE
│ └── feature_request.md
├── .gitignore
├── .idea
├── .gitignore
├── misc.xml
├── modules.xml
├── stringFormatter.iml
└── vcs.xml
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── formatter.go
├── formatter_benchmark_test.go
├── formatter_test.go
├── go.mod
├── go.sum
├── img
├── benchmarks.png
├── benchmarks2.png
├── benchmarks_adv.png
├── map2str_benchmarks.png
├── sf_cover.png
└── slice2str_benchmarks.png
├── maptostring.go
├── maptostring_benchmark_test.go
├── maptostring_test.go
├── slicetostring.go
├── slicetostring_benchmark_test.go
├── slicetostring_test.go
└── utils
└── run_benchamrks.ps1
/.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 |
--------------------------------------------------------------------------------
/.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/.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 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/stringFormatter.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | Slice printing is **50% faster with 8 items** slice and **250% with 20 items** slice
5 |
6 | 
7 | 
8 | 
9 | 
10 | 
11 |
12 | 
13 |
14 | ## 1. Features
15 |
16 | 1. Text formatting with template using traditional for `C#, Python programmers style` - `{0}`, `{name}` that faster then `fmt` does:
17 | 
18 | 2. Additional text utilities:
19 | - convert ***map to string*** using one of predefined formats (see `text_utils.go`)
20 |
21 | ### 1. Text formatting from templates
22 |
23 | #### 1.1 Description
24 |
25 | This is a GO module for ***template text formatting in syntax like in C# or/and Python*** using:
26 | - `{n}` , n here is a number to notes order of argument list to use i.e. `{0}`, `{1}`
27 | - `{name}` to notes arguments by name i.e. `{name}`, `{last_name}`, `{address}` and so on ...
28 |
29 | #### 1.2 Examples
30 |
31 | ##### 1.2.1 Format by arg order
32 |
33 | i.e. you have following template: `"Hello {0}, we are greeting you here: {1}!"`
34 |
35 | if you call Format with args "manager" and "salesApp" :
36 |
37 | ```go
38 | formattedStr := stringFormatter.Format("Hello {0}, we are greeting you here: {1}!", "manager", "salesApp")
39 | ```
40 |
41 | you get string `"Hello manager, we are greeting you here: salesApp!"`
42 |
43 | ##### 1.2.2 Format by arg key
44 |
45 | i.e. you have following template: `"Hello {user} what are you doing here {app} ?"`
46 |
47 | 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"})`
48 |
49 | you get string `"Hello vpupkin what are you doing here mn_console ?"`
50 |
51 | another example is:
52 |
53 | ```go
54 | strFormatResult = stringFormatter.FormatComplex(
55 | "Current app settings are: ipAddr: {ipaddr}, port: {port}, use ssl: {ssl}.",
56 | map[string]any{"ipaddr":"127.0.0.1", "port":5432, "ssl":false},
57 | )
58 | ```
59 | a result will be: `"Current app settings are: ipAddr: 127.0.0.1, port: 5432, use ssl: false."``
60 |
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 | 6. Lists
81 | - `{0:L-}, [1,2,3] outputs -> 1-2-3`
82 | - `{0:L, }, [1,2,3] outputs -> 1, 2, 3`
83 |
84 | ##### 1.2.4 Benchmarks of the Format and FormatComplex functions
85 |
86 | benchmark could be running using following commands from command line:
87 | * to see `Format` result - `go test -bench=Format -benchmem -cpu 1`
88 | * to see `fmt` result - `go test -bench=Fmt -benchmem -cpu 1`
89 |
90 | ### 2. Text utilities
91 |
92 | #### 2.1 Map to string utility
93 |
94 | `MapToString` function allows to convert map with primitive key to string using format, including key and value, e.g.:
95 | * `{key} => {value}`
96 | * `{key} : {value}`
97 | * `{value}`
98 |
99 | For example:
100 | ```go
101 | options := map[string]any{
102 | "connectTimeout": 1000,
103 | "useSsl": true,
104 | "login": "sa",
105 | "password": "sa",
106 | }
107 |
108 | str := stringFormatter.MapToString(&options, "{key} : {value}", ", ")
109 | // NOTE: order of key-value pairs is not guranteed though
110 | // str will be something like:
111 | "connectTimeout : 1000, useSsl : true, login : sa, password : sa"
112 | ```
113 |
114 | #### 2.2 Benchmarks of the MapToString function
115 |
116 | * to see `MapToStr` result - `go test -bench=MapToStr -benchmem -cpu 1`
117 |
118 | 
119 |
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 | `SliceSameTypeToString` - function that converts typed slice to line with separator
130 | ```go
131 | separator := ":"
132 | numericSlice := []int{100, 200, 400, 800}
133 | result := stringFormatter.SliceSameTypeToString(&numericSlice, &separator)
134 | ```
135 |
136 | #### 2.4 Benchmarks of the SliceToString function
137 |
138 | `sf` is rather fast then `fmt` 2.5 times (250%) faster on slice with 20 items, see benchmark:
139 | 
140 |
141 | ### 3. Contributors
142 |
143 |
144 |
145 |
146 |
--------------------------------------------------------------------------------
/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 | // Format
13 | /* Func that makes string formatting from template
14 | * It differs from above function only by generic interface that allow to use only primitive data types:
15 | * - integers (int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uin64)
16 | * - floats (float32, float64)
17 | * - boolean
18 | * - string
19 | * - complex
20 | * - objects
21 | * This function defines format automatically
22 | * Parameters
23 | * - template - string that contains template
24 | * - args - values that are using for formatting with template
25 | * Returns formatted string
26 | */
27 | func Format(template string, args ...any) string {
28 | if args == nil {
29 | return template
30 | }
31 |
32 | start := strings.Index(template, "{")
33 | if start < 0 {
34 | return template
35 | }
36 |
37 | templateLen := len(template)
38 | formattedStr := &strings.Builder{}
39 | argsLen := bytesPerArgDefault * len(args)
40 | formattedStr.Grow(templateLen + argsLen + 1)
41 | j := -1 //nolint:ineffassign
42 |
43 | nestedBrackets := false
44 | formattedStr.WriteString(template[:start])
45 | for i := start; i < templateLen; i++ {
46 | if template[i] == '{' {
47 | // possibly it is a template placeholder
48 | if i == templateLen-1 {
49 | // if we gave { at the end of line i.e. -> type serviceHealth struct {,
50 | // without this write we got type serviceHealth struct
51 | formattedStr.WriteByte('{')
52 | break
53 | }
54 | // considering in 2 phases - {{ }}
55 | if template[i+1] == '{' {
56 | formattedStr.WriteByte('{')
57 | continue
58 | }
59 | // find end of placeholder
60 | // process empty pair - {}
61 | if template[i+1] == '}' {
62 | i++
63 | formattedStr.WriteString("{}")
64 | continue
65 | }
66 | // process non-empty placeholder
67 | j = i + 2
68 | for {
69 | if j >= templateLen {
70 | break
71 | }
72 |
73 | if template[j] == '{' {
74 | // multiple nested curly brackets ...
75 | nestedBrackets = true
76 | formattedStr.WriteString(template[i:j])
77 | i = j
78 | }
79 |
80 | if template[j] == '}' {
81 | break
82 | }
83 |
84 | j++
85 | }
86 | // double curly brackets processed here, convert {{N}} -> {N}
87 | // so we catch here {{N}
88 | if j+1 < templateLen && template[j+1] == '}' && template[i-1] == '{' {
89 | formattedStr.WriteString(template[i+1 : j+1])
90 | i = j + 1
91 | } else {
92 | argNumberStr := template[i+1 : j]
93 | // is here we should support formatting ?
94 | var argNumber int
95 | var err error
96 | var argFormatOptions string
97 | if len(argNumberStr) == 1 {
98 | // this calculation makes work a little faster than AtoI
99 | argNumber = int(argNumberStr[0] - '0')
100 | } else {
101 | argNumber = -1
102 | // Here we are going to process argument either with additional formatting or not
103 | // i.e. 0 for arg without formatting && 0:format for an argument wit formatting
104 | // todo(UMV): we could format json or yaml here ...
105 | formatOptionIndex := strings.Index(argNumberStr, argumentFormatSeparator)
106 | // formatOptionIndex can't be == 0, because 0 is a position of arg number
107 | if formatOptionIndex > 0 {
108 | // trimmed was down later due to we could format list with space separator
109 | argFormatOptions = argNumberStr[formatOptionIndex+1:]
110 | argNumberStrPart := argNumberStr[:formatOptionIndex]
111 | argNumber, err = strconv.Atoi(strings.Trim(argNumberStrPart, " "))
112 | if err == nil {
113 | argNumberStr = argNumberStrPart
114 | }
115 | // make formatting option str for further pass to an argument
116 | }
117 | //
118 | if argNumber < 0 {
119 | argNumber, err = strconv.Atoi(argNumberStr)
120 | }
121 | }
122 |
123 | if (err == nil || (argFormatOptions != "" && !nestedBrackets)) &&
124 | len(args) > argNumber {
125 | // get number from placeholder
126 | strVal := getItemAsStr(&args[argNumber], &argFormatOptions)
127 | formattedStr.WriteString(strVal)
128 | } else {
129 | formattedStr.WriteString(template[i:j])
130 | if j < templateLen-1 {
131 | formattedStr.WriteByte(template[j])
132 | }
133 | }
134 | i = j
135 | }
136 | } else {
137 | j = i //nolint:ineffassign
138 | formattedStr.WriteByte(template[i])
139 | }
140 | }
141 |
142 | return formattedStr.String()
143 | }
144 |
145 | // FormatComplex
146 | /* Function that format text using more complex templates contains string literals i.e "Hello {username} here is our application {appname}
147 | * Parameters
148 | * - template - string that contains template
149 | * - args - values (dictionary: string key - any value) that are using for formatting with template
150 | * Returns formatted string
151 | */
152 | func FormatComplex(template string, args map[string]any) string {
153 | if args == nil {
154 | return template
155 | }
156 |
157 | start := strings.Index(template, "{")
158 | if start < 0 {
159 | return template
160 | }
161 |
162 | templateLen := len(template)
163 | formattedStr := &strings.Builder{}
164 | argsLen := bytesPerArgDefault * len(args)
165 | formattedStr.Grow(templateLen + argsLen + 1)
166 | j := -1 //nolint:ineffassign
167 | nestedBrackets := false
168 | formattedStr.WriteString(template[:start])
169 | for i := start; i < templateLen; i++ {
170 | if template[i] == '{' {
171 | // possibly it is a template placeholder
172 | if i == templateLen-1 {
173 | // if we gave { at the end of line i.e. -> type serviceHealth struct {,
174 | // without this write we got type serviceHealth struct
175 | formattedStr.WriteByte('{')
176 | break
177 | }
178 |
179 | if template[i+1] == '{' {
180 | formattedStr.WriteByte('{')
181 | continue
182 | }
183 | // find end of placeholder
184 | // process empty pair - {}
185 | if template[i+1] == '}' {
186 | i++
187 | formattedStr.WriteString("{}")
188 | continue
189 | }
190 | // process non-empty placeholder
191 |
192 | // find end of placeholder
193 | j = i + 2
194 | for {
195 | if j >= templateLen {
196 | break
197 | }
198 | if template[j] == '{' {
199 | // multiple nested curly brackets ...
200 | nestedBrackets = true
201 | formattedStr.WriteString(template[i:j])
202 | i = j
203 | }
204 | if template[j] == '}' {
205 | break
206 | }
207 | j++
208 | }
209 | // double curly brackets processed here, convert {{N}} -> {N}
210 | // so we catch here {{N}
211 | if j+1 < templateLen && template[j+1] == '}' {
212 | formattedStr.WriteString(template[i+1 : j+1])
213 | i = j + 1
214 | } else {
215 | var argFormatOptions string
216 | argNumberStr := template[i+1 : j]
217 | arg, ok := args[argNumberStr]
218 | if !ok {
219 | formatOptionIndex := strings.Index(argNumberStr, argumentFormatSeparator)
220 | if formatOptionIndex >= 0 {
221 | // argFormatOptions = strings.Trim(argNumberStr[formatOptionIndex+1:], " ")
222 | argFormatOptions = argNumberStr[formatOptionIndex+1:]
223 | argNumberStr = strings.Trim(argNumberStr[:formatOptionIndex], " ")
224 | }
225 |
226 | arg, ok = args[argNumberStr]
227 | }
228 | if ok || (argFormatOptions != "" && !nestedBrackets) {
229 | // get number from placeholder
230 | strVal := ""
231 | if arg != nil {
232 | strVal = getItemAsStr(&arg, &argFormatOptions)
233 | } else {
234 | formattedStr.WriteString(template[i:j])
235 | if j < templateLen-1 {
236 | formattedStr.WriteByte(template[j])
237 | }
238 | }
239 | formattedStr.WriteString(strVal)
240 | } else {
241 | formattedStr.WriteString(template[i:j])
242 | if j < templateLen-1 {
243 | formattedStr.WriteByte(template[j])
244 | }
245 | }
246 | i = j
247 | }
248 | } else {
249 | j = i //nolint:ineffassign
250 | formattedStr.WriteByte(template[i])
251 | }
252 | }
253 |
254 | return formattedStr.String()
255 | }
256 |
257 | func getItemAsStr(item *any, itemFormat *string) string {
258 | base := 10
259 | var floatFormat byte = 'f'
260 | precision := -1
261 | var preparedArgFormat string
262 | var argStr string
263 | postProcessingRequired := false
264 | intNumberFormat := false
265 | floatNumberFormat := false
266 |
267 | if itemFormat != nil && len(*itemFormat) > 0 {
268 | /* for numbers there are following formats:
269 | * d(D) - decimal
270 | * b(B) - binary
271 | * f(F) - fixed point i.e {0:F}, 10.5467890 -> 10.546789 ; {0:F4}, 10.5467890 -> 10.5468
272 | * e(E) - exponential - float point with scientific format {0:E2}, 191.0784 -> 1.91e+02
273 | * x(X) - hexadecimal i.e. {0:X}, 250 -> fa ; {0:X4}, 250 -> 00fa
274 | * p(P) - percent i.e. {0:P100}, 12 -> 12%
275 | * Following formats are not supported yet:
276 | * 1. c(C) currency it requires also country code
277 | * 2. g(G),and others with locales
278 | * 3. f(F) - fixed point, {0,F4}, 123.15 -> 123.1500
279 | * OUR own addition:
280 | * 1. O(o) - octahedral number format
281 | */
282 | // preparedArgFormat is trimmed format, L type could contain spaces
283 | preparedArgFormat = strings.Trim(*itemFormat, " ")
284 | postProcessingRequired = len(preparedArgFormat) > 1
285 |
286 | switch rune(preparedArgFormat[0]) {
287 | case 'd', 'D':
288 | base = 10
289 | intNumberFormat = true
290 | case 'x', 'X':
291 | base = 16
292 | intNumberFormat = true
293 | case 'o', 'O':
294 | base = 8
295 | intNumberFormat = true
296 | case 'b', 'B':
297 | base = 2
298 | intNumberFormat = true
299 | case 'e', 'E', 'f', 'F':
300 | if rune(preparedArgFormat[0]) == 'e' || rune(preparedArgFormat[0]) == 'E' {
301 | floatFormat = 'e'
302 | }
303 | // precision was passed, take [1:end], extract precision
304 | if postProcessingRequired {
305 | precisionStr := preparedArgFormat[1:]
306 | precisionVal, err := strconv.Atoi(precisionStr)
307 | if err == nil {
308 | precision = precisionVal
309 | }
310 | }
311 | postProcessingRequired = false
312 | floatNumberFormat = floatFormat == 'f'
313 |
314 | case 'p', 'P':
315 | // percentage processes here ...
316 | if postProcessingRequired {
317 | dividerStr := preparedArgFormat[1:]
318 | dividerVal, err := strconv.ParseFloat(dividerStr, 32)
319 | if err == nil {
320 | // 1. Convert arg to float
321 | val := (*item).(interface{})
322 | var floatVal float64
323 | switch val.(type) {
324 | case float64:
325 | floatVal = val.(float64)
326 | case int:
327 | floatVal = float64(val.(int))
328 | default:
329 | floatVal = 0
330 | }
331 | // 2. Divide arg / divider and multiply by 100
332 | percentage := (floatVal / dividerVal) * 100
333 | return strconv.FormatFloat(percentage, floatFormat, 2, 64)
334 | }
335 | }
336 | // l(L) is for list(slice)
337 | case 'l', 'L':
338 | separator := ","
339 | if len(*itemFormat) > 1 {
340 | separator = (*itemFormat)[1:]
341 | }
342 |
343 | // slice processing converting to {item}{delimiter}{item}{delimiter}{item}
344 | slice, ok := (*item).([]any)
345 | if ok {
346 | if len(slice) == 1 {
347 | // this is because slice in 0 item contains another slice, we should take it
348 | slice, ok = slice[0].([]any)
349 | }
350 | return SliceToString(&slice, &separator)
351 | } else {
352 | return convertSliceToStrWithTypeDiscover(item, &separator)
353 | }
354 | default:
355 | base = 10
356 | }
357 | }
358 |
359 | switch v := (*item).(type) {
360 | case string:
361 | argStr = v
362 | case int8:
363 | argStr = strconv.FormatInt(int64(v), base)
364 | case int16:
365 | argStr = strconv.FormatInt(int64(v), base)
366 | case int32:
367 | argStr = strconv.FormatInt(int64(v), base)
368 | case int64:
369 | argStr = strconv.FormatInt(v, base)
370 | case int:
371 | argStr = strconv.FormatInt(int64(v), base)
372 | case uint8:
373 | argStr = strconv.FormatUint(uint64(v), base)
374 | case uint16:
375 | argStr = strconv.FormatUint(uint64(v), base)
376 | case uint32:
377 | argStr = strconv.FormatUint(uint64(v), base)
378 | case uint64:
379 | argStr = strconv.FormatUint(v, base)
380 | case uint:
381 | argStr = strconv.FormatUint(uint64(v), base)
382 | case bool:
383 | argStr = strconv.FormatBool(v)
384 | case float32:
385 | argStr = strconv.FormatFloat(float64(v), floatFormat, precision, 32)
386 | case float64:
387 | argStr = strconv.FormatFloat(v, floatFormat, precision, 64)
388 | default:
389 | argStr = fmt.Sprintf("%v", v)
390 | }
391 |
392 | if !postProcessingRequired {
393 | return argStr
394 | }
395 |
396 | // 1. If integer numbers add filling
397 | if intNumberFormat {
398 | symbolsStr := preparedArgFormat[1:]
399 | symbolsStrVal, err := strconv.Atoi(symbolsStr)
400 | if err == nil {
401 | symbolsToAdd := symbolsStrVal - len(argStr)
402 | if symbolsToAdd > 0 {
403 | advArgStr := strings.Builder{}
404 | advArgStr.Grow(len(argStr) + symbolsToAdd + 1)
405 |
406 | for i := 0; i < symbolsToAdd; i++ {
407 | advArgStr.WriteByte('0')
408 | }
409 | advArgStr.WriteString(argStr)
410 | return advArgStr.String()
411 | }
412 | }
413 | }
414 |
415 | if floatNumberFormat && precision > 0 {
416 | pointIndex := strings.Index(argStr, ".")
417 | if pointIndex > 0 {
418 | advArgStr := strings.Builder{}
419 | advArgStr.Grow(len(argStr) + precision + 1)
420 | advArgStr.WriteString(argStr)
421 | numberOfSymbolsAfterPoint := len(argStr) - (pointIndex + 1)
422 | for i := numberOfSymbolsAfterPoint; i < precision; i++ {
423 | advArgStr.WriteByte(0)
424 | }
425 | return advArgStr.String()
426 | }
427 | }
428 |
429 | return argStr
430 | }
431 |
432 | func convertSliceToStrWithTypeDiscover(slice *any, separator *string) string {
433 | // 1. attempt to convert to int
434 | iSlice, ok := (*slice).([]int)
435 | if ok {
436 | return SliceSameTypeToString(&iSlice, separator)
437 | }
438 |
439 | // 2. attempt to convert to string
440 | sSlice, ok := (*slice).([]string)
441 | if ok {
442 | return SliceSameTypeToString(&sSlice, separator)
443 | }
444 |
445 | // 3. attempt to convert to float64
446 | f64Slice, ok := (*slice).([]float64)
447 | if ok {
448 | return SliceSameTypeToString(&f64Slice, separator)
449 | }
450 |
451 | // 4. attempt to convert to float32
452 | f32Slice, ok := (*slice).([]float32)
453 | if ok {
454 | return SliceSameTypeToString(&f32Slice, separator)
455 | }
456 |
457 | // 5. attempt to convert to bool
458 | bSlice, ok := (*slice).([]bool)
459 | if ok {
460 | return SliceSameTypeToString(&bSlice, separator)
461 | }
462 |
463 | // 6. attempt to convert to int64
464 | i64Slice, ok := (*slice).([]int64)
465 | if ok {
466 | return SliceSameTypeToString(&i64Slice, separator)
467 | }
468 |
469 | // 7. attempt to convert to uint
470 | uiSlice, ok := (*slice).([]uint)
471 | if ok {
472 | return SliceSameTypeToString(&uiSlice, separator)
473 | }
474 |
475 | // 8. attempt to convert to int32
476 | i32Slice, ok := (*slice).([]int32)
477 | if ok {
478 | return SliceSameTypeToString(&i32Slice, separator)
479 | }
480 |
481 | // default way ...
482 | return fmt.Sprintf("%v", *slice)
483 | }
484 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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" {0}, ` +
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 | "struct arg": {
105 | template: "Example is: {0}",
106 | args: []any{
107 | meteoData{
108 | Int: 123,
109 | Str: "This is a test str, nothing more special",
110 | Double: -1.098743,
111 | Err: errors.New("main question error, is 42"),
112 | },
113 | },
114 | expected: "Example is: {123 This is a test str, nothing more special -1.098743 main question error, is 42}",
115 | },
116 | "open bracket at the end of line of go line": {
117 | template: "type serviceHealth struct {",
118 | args: []any{},
119 | expected: "type serviceHealth struct {",
120 | },
121 | "open bracket at the end of line of go line with {} inside": {
122 | template: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) {",
123 | args: []any{},
124 | expected: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) {",
125 | },
126 |
127 | "close bracket at the end of line of go line with {} inside": {
128 | template: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) }",
129 | args: []any{},
130 | expected: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) }",
131 | },
132 |
133 | "no bracket at the end of line with {} inside": {
134 | template: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) ",
135 | args: []any{},
136 | expected: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) ",
137 | },
138 | "open bracket at the end of line of go line with multiple {} inside": {
139 | template: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}, additionalData interface{}) {",
140 | args: []any{},
141 | expected: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}, additionalData interface{}) {",
142 | },
143 | "commentaries after bracket": {
144 | template: "switch app.appConfig.ServerCfg.Schema { //nolint:exhaustive",
145 | args: []any{},
146 | expected: "switch app.appConfig.ServerCfg.Schema { //nolint:exhaustive",
147 | },
148 | "bracket in the middle": {
149 | template: "in the middle - { at the end - nothing",
150 | args: []any{},
151 | expected: "in the middle - { at the end - nothing",
152 | },
153 | } {
154 | t.Run(name, func(t *testing.T) {
155 | assert.Equal(t, test.expected, stringFormatter.Format(test.template, test.args...))
156 | })
157 | }
158 | }
159 |
160 | func TestFormatWithArgFormatting(t *testing.T) {
161 | for name, test := range map[string]struct {
162 | template string
163 | args []any
164 | expected string
165 | }{
166 | "numeric_test_1": {
167 | template: "This is the text with an only number formatting: decimal - {0} / {0 : D6}, scientific - {1} / {1 : e2}",
168 | args: []any{123, 191.0784},
169 | expected: "This is the text with an only number formatting: decimal - 123 / 000123, scientific - 191.0784 / 1.91e+02",
170 | },
171 | "numeric_test_2": {
172 | template: "This is the text with an only number formatting: binary - {0:B} / {0 : B8}, hexadecimal - {1:X} / {1 : X4}",
173 | args: []any{15, 250},
174 | expected: "This is the text with an only number formatting: binary - 1111 / 00001111, hexadecimal - fa / 00fa",
175 | },
176 | "numeric_test_3": {
177 | template: "This is the text with an only number formatting: decimal - {0:F} / {0 : F4} / {0:F8}",
178 | args: []any{10.5467890},
179 | expected: "This is the text with an only number formatting: decimal - 10.546789 / 10.5468 / 10.54678900",
180 | },
181 | "numeric_test_4": {
182 | template: "This is the text with percentage format - {0:P100} / {0 : P100.5}, and non normal percentage {1:P100}",
183 | args: []any{12, "ass"},
184 | expected: "This is the text with percentage format - 12.00 / 11.94, and non normal percentage 0.00",
185 | },
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 | },
201 | } {
202 | // Run test here
203 | t.Run(name, func(t *testing.T) {
204 | // assert.NotNil(t, test)
205 | assert.Equal(t, test.expected, stringFormatter.Format(test.template, test.args...))
206 | })
207 | }
208 | }
209 |
210 | func TestFormatWithArgFormattingForTypedSlice(t *testing.T) {
211 | for name, test := range map[string]struct {
212 | template string
213 | args []any
214 | expected string
215 | }{
216 | "list_with_int_slice": {
217 | template: "This is a list(slice) test: {0:L-}",
218 | args: []any{[]int{101, 202, 303}},
219 | expected: "This is a list(slice) test: 101-202-303",
220 | },
221 | "list_with_uint_slice": {
222 | template: "This is a list(slice) test: {0:L-}",
223 | args: []any{[]uint{102, 204, 308}},
224 | expected: "This is a list(slice) test: 102-204-308",
225 | },
226 | "list_with_int32_slice": {
227 | template: "This is a list(slice) test: {0:L-}",
228 | args: []any{[]int32{100, 200, 300}},
229 | expected: "This is a list(slice) test: 100-200-300",
230 | },
231 | "list_with_int64_slice": {
232 | template: "This is a list(slice) test: {0:L-}",
233 | args: []any{[]int64{1001, 2002, 3003}},
234 | expected: "This is a list(slice) test: 1001-2002-3003",
235 | },
236 | "list_with_float64_slice": {
237 | template: "This is a list(slice) test: {0:L-}",
238 | args: []any{[]float64{1.01, 2.02, 3.03}},
239 | expected: "This is a list(slice) test: 1.01-2.02-3.03",
240 | },
241 | "list_with_float32_slice": {
242 | template: "This is a list(slice) test: {0:L-}",
243 | args: []any{[]float32{5.01, 6.02, 7.03}},
244 | expected: "This is a list(slice) test: 5.01-6.02-7.03",
245 | },
246 | "list_with_bool_slice": {
247 | template: "This is a list(slice) test: {0:L-}",
248 | args: []any{[]bool{true, true, false}},
249 | expected: "This is a list(slice) test: true-true-false",
250 | },
251 | "list_with_string_slice": {
252 | template: "This is a list(slice) test: {0:L-}",
253 | args: []any{[]string{"s1", "s2", "s3"}},
254 | expected: "This is a list(slice) test: s1-s2-s3",
255 | },
256 | } {
257 | // Run test here
258 | t.Run(name, func(t *testing.T) {
259 | // assert.NotNil(t, test)
260 | assert.Equal(t, test.expected, stringFormatter.Format(test.template, test.args...))
261 | })
262 | }
263 | }
264 |
265 | // TestStrFormatWithComplicatedText - this test represents issue with complicated text
266 | func TestFormatComplex(t *testing.T) {
267 | for name, test := range map[string]struct {
268 | template string
269 | args map[string]any
270 | expected string
271 | }{
272 | "numeric_test_1": {
273 | template: `
274 | {
275 | "Comment": "Call Lambda with GRPC",
276 | "StartAt": "CallLambdaWithGrpc",
277 | "States": {"CallLambdaWithGrpc": {"Type": "Task", "Resource": "{address}:get ad user", "End": true}}
278 | }`,
279 | args: map[string]any{"address": _address},
280 | expected: `
281 | {
282 | "Comment": "Call Lambda with GRPC",
283 | "StartAt": "CallLambdaWithGrpc",
284 | "States": {"CallLambdaWithGrpc": {"Type": "Task", "Resource": "grpcs://127.0.0.1:get ad user", "End": true}}
285 | }`,
286 | },
287 | "key not found": {
288 | template: "Hello: {username}, you earn {amount} $",
289 | args: map[string]any{"amount": 1000},
290 | expected: "Hello: {username}, you earn 1000 $",
291 | },
292 | "dialog": {
293 | template: "Hello {user} what are you doing here {app} ?",
294 | args: map[string]any{"user": "vpupkin", "app": "mn_console"},
295 | expected: "Hello vpupkin what are you doing here mn_console ?",
296 | },
297 | "info message": {
298 | template: "Current app settings are: ipAddr: {ipaddr}, port: {port}, use ssl: {ssl}.",
299 | args: map[string]any{"ipaddr": "127.0.0.1", "port": 5432, "ssl": false},
300 | expected: "Current app settings are: ipAddr: 127.0.0.1, port: 5432, use ssl: false.",
301 | },
302 | "one json line with open bracket at the end": {
303 | template: " \"server\": {",
304 | args: map[string]any{},
305 | expected: " \"server\": {",
306 | },
307 | "open bracket at the end of line of go line with {} inside": {
308 | template: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) {",
309 | args: map[string]any{},
310 | expected: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) {",
311 | },
312 |
313 | "open bracket at the end of line of go line with multiple {} inside": {
314 | template: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}, additionalData interface{}) {",
315 | args: map[string]any{},
316 | expected: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}, additionalData interface{}) {",
317 | },
318 |
319 | "close bracket at the end of line of go line with {} inside": {
320 | template: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) }",
321 | args: map[string]any{},
322 | expected: "func afterHandle(respWriter *http.ResponseWriter, statusCode int, data interface{}) }",
323 | },
324 | "commentaries after bracket": {
325 | template: "switch app.appConfig.ServerCfg.Schema { //nolint:exhaustive",
326 | args: map[string]any{},
327 | expected: "switch app.appConfig.ServerCfg.Schema { //nolint:exhaustive",
328 | },
329 | } {
330 | t.Run(name, func(t *testing.T) {
331 | assert.Equal(t, test.expected, stringFormatter.FormatComplex(test.template, test.args))
332 | })
333 | }
334 | }
335 |
336 | func TestFormatComplexWithArgFormatting(t *testing.T) {
337 | for name, test := range map[string]struct {
338 | template string
339 | args map[string]any
340 | expected string
341 | }{
342 | "numeric_test_1": {
343 | template: "This is the text with an only number formatting: scientific - {mass} / {mass : e2}",
344 | args: map[string]any{"mass": 191.0784},
345 | expected: "This is the text with an only number formatting: scientific - 191.0784 / 1.91e+02",
346 | },
347 | "numeric_test_2": {
348 | template: "This is the text with an only number formatting: binary - {bin:B} / {bin : B8}, hexadecimal - {hex:X} / {hex : X4}",
349 | args: map[string]any{"bin": 15, "hex": 250},
350 | expected: "This is the text with an only number formatting: binary - 1111 / 00001111, hexadecimal - fa / 00fa",
351 | },
352 | "numeric_test_3": {
353 | template: "This is the text with an only number formatting: decimal - {float:F} / {float : F4} / {float:F8}",
354 | args: map[string]any{"float": 10.5467890},
355 | expected: "This is the text with an only number formatting: decimal - 10.546789 / 10.5468 / 10.54678900",
356 | },
357 | "list_with_default_sep": {
358 | template: "This is a list(slice) test: {list:L}",
359 | args: map[string]any{"list": []any{"s1", "s2", "s3"}},
360 | expected: "This is a list(slice) test: s1,s2,s3",
361 | },
362 | "list_with_dash_sep": {
363 | template: "This is a list(slice) test: {list:L-}",
364 | args: map[string]any{"list": []any{"s1", "s2", "s3"}},
365 | expected: "This is a list(slice) test: s1-s2-s3",
366 | },
367 | "list_with_space_sep": {
368 | template: "This is a list(slice) test: {list:L }",
369 | args: map[string]any{"list": []any{"s1", "s2", "s3"}},
370 | expected: "This is a list(slice) test: s1 s2 s3",
371 | },
372 | } {
373 | t.Run(name, func(t *testing.T) {
374 | assert.Equal(t, test.expected, stringFormatter.FormatComplex(test.template, test.args))
375 | })
376 | }
377 | }
378 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/img/benchmarks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wissance/stringFormatter/ebb0f476a9c992e5b673d7928136d84901e4c496/img/benchmarks.png
--------------------------------------------------------------------------------
/img/benchmarks2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wissance/stringFormatter/ebb0f476a9c992e5b673d7928136d84901e4c496/img/benchmarks2.png
--------------------------------------------------------------------------------
/img/benchmarks_adv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wissance/stringFormatter/ebb0f476a9c992e5b673d7928136d84901e4c496/img/benchmarks_adv.png
--------------------------------------------------------------------------------
/img/map2str_benchmarks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wissance/stringFormatter/ebb0f476a9c992e5b673d7928136d84901e4c496/img/map2str_benchmarks.png
--------------------------------------------------------------------------------
/img/sf_cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wissance/stringFormatter/ebb0f476a9c992e5b673d7928136d84901e4c496/img/sf_cover.png
--------------------------------------------------------------------------------
/img/slice2str_benchmarks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wissance/stringFormatter/ebb0f476a9c992e5b673d7928136d84901e4c496/img/slice2str_benchmarks.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------