├── .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 | 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 | ![GitHub go.mod Go version (subdirectory of monorepo)](https://img.shields.io/github/go-mod/go-version/wissance/stringFormatter?style=plastic) 7 | ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/wissance/stringFormatter?style=plastic) 8 | ![GitHub issues](https://img.shields.io/github/issues/wissance/stringFormatter?style=plastic) 9 | ![GitHub Release Date](https://img.shields.io/github/release-date/wissance/stringFormatter) 10 | ![GitHub release (latest by date)](https://img.shields.io/github/downloads/wissance/stringFormatter/v1.4.1/total?style=plastic) 11 | 12 | ![String Formatter: a convenient string formatting tool](img/sf_cover.png) 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 | ![String Formatter: a convenient string formatting tool](img/benchmarks_adv.png) 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 | ![MapToStr benchmarks](/img/map2str_benchmarks.png) 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 | ![SliceToStr benchmarks](/img/slice2str_benchmarks.png) 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 --------------------------------------------------------------------------------