├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── FormatWith.sln ├── FormatWith ├── FormatWith-Release.snk ├── FormatWith.csproj ├── Internal │ ├── FormatHelpers.cs │ ├── FormatToken.cs │ ├── FormatWithMethods.cs │ └── StringBuilderExtensions.cs ├── MissingKeyBehaviour.cs ├── ReplacementResult.cs └── StringExtensions.cs ├── FormatWithTests ├── FormatProvider │ └── UpperCaseFormatProvider.cs ├── FormatWithTests.cs ├── FormatWithTests.csproj ├── FormattableWithTests.cs ├── MiscTests.cs └── TestStrings.cs ├── License.txt ├── README.md └── icon.png /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | os: ['windows-2019', 'ubuntu-20.04'] 16 | runs-on: ${{ matrix.os }} 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Build and Test 25 | run: dotnet test -c release 26 | shell: pwsh 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | # DNX 42 | project.lock.json 43 | artifacts/ 44 | 45 | *_i.c 46 | *_p.c 47 | *_i.h 48 | *.ilk 49 | *.meta 50 | *.obj 51 | *.pch 52 | *.pdb 53 | *.pgc 54 | *.pgd 55 | *.rsp 56 | *.sbr 57 | *.tlb 58 | *.tli 59 | *.tlh 60 | *.tmp 61 | *.tmp_proj 62 | *.log 63 | *.vspscc 64 | *.vssscc 65 | .builds 66 | *.pidb 67 | *.svclog 68 | *.scc 69 | 70 | # Chutzpah Test files 71 | _Chutzpah* 72 | 73 | # Visual C++ cache files 74 | ipch/ 75 | *.aps 76 | *.ncb 77 | *.opensdf 78 | *.sdf 79 | *.cachefile 80 | 81 | # Visual Studio profiler 82 | *.psess 83 | *.vsp 84 | *.vspx 85 | 86 | # TFS 2012 Local Workspace 87 | $tf/ 88 | 89 | # Guidance Automation Toolkit 90 | *.gpState 91 | 92 | # ReSharper is a .NET coding add-in 93 | _ReSharper*/ 94 | *.[Rr]e[Ss]harper 95 | *.DotSettings.user 96 | 97 | # JustCode is a .NET coding add-in 98 | .JustCode 99 | 100 | # TeamCity is a build add-in 101 | _TeamCity* 102 | 103 | # DotCover is a Code Coverage Tool 104 | *.dotCover 105 | 106 | # NCrunch 107 | _NCrunch_* 108 | .*crunch*.local.xml 109 | 110 | # MightyMoose 111 | *.mm.* 112 | AutoTest.Net/ 113 | 114 | # Web workbench (sass) 115 | .sass-cache/ 116 | 117 | # Installshield output folder 118 | [Ee]xpress/ 119 | 120 | # DocProject is a documentation generator add-in 121 | DocProject/buildhelp/ 122 | DocProject/Help/*.HxT 123 | DocProject/Help/*.HxC 124 | DocProject/Help/*.hhc 125 | DocProject/Help/*.hhk 126 | DocProject/Help/*.hhp 127 | DocProject/Help/Html2 128 | DocProject/Help/html 129 | 130 | # Click-Once directory 131 | publish/ 132 | 133 | # Publish Web Output 134 | *.[Pp]ublish.xml 135 | *.azurePubxml 136 | ## TODO: Comment the next line if you want to checkin your 137 | ## web deploy settings but do note that will include unencrypted 138 | ## passwords 139 | #*.pubxml 140 | 141 | *.publishproj 142 | 143 | # NuGet Packages 144 | *.nupkg 145 | # The packages folder can be ignored because of Package Restore 146 | **/packages/* 147 | # except build/, which is used as an MSBuild target. 148 | !**/packages/build/ 149 | # Uncomment if necessary however generally it will be regenerated when needed 150 | #!**/packages/repositories.config 151 | 152 | # Windows Azure Build Output 153 | csx/ 154 | *.build.csdef 155 | 156 | # Windows Store app package directory 157 | AppPackages/ 158 | 159 | # Visual Studio cache files 160 | # files ending in .cache can be ignored 161 | *.[Cc]ache 162 | # but keep track of directories ending in .cache 163 | !*.[Cc]ache/ 164 | 165 | # Others 166 | ClientBin/ 167 | [Ss]tyle[Cc]op.* 168 | ~$* 169 | *~ 170 | *.dbmdl 171 | *.dbproj.schemaview 172 | *.pfx 173 | *.publishsettings 174 | node_modules/ 175 | orleans.codegen.cs 176 | 177 | # RIA/Silverlight projects 178 | Generated_Code/ 179 | 180 | # Backup & report files from converting an old project file 181 | # to a newer Visual Studio version. Backup files are not needed, 182 | # because we have git ;-) 183 | _UpgradeReport_Files/ 184 | Backup*/ 185 | UpgradeLog*.XML 186 | UpgradeLog*.htm 187 | 188 | # SQL Server files 189 | *.mdf 190 | *.ldf 191 | 192 | # Business Intelligence projects 193 | *.rdl.data 194 | *.bim.layout 195 | *.bim_*.settings 196 | 197 | # Microsoft Fakes 198 | FakesAssemblies/ 199 | 200 | # Node.js Tools for Visual Studio 201 | .ntvs_analysis.dat 202 | 203 | # Visual Studio 6 build log 204 | *.plg 205 | 206 | # Visual Studio 6 workspace options file 207 | *.opt 208 | 209 | # LightSwitch generated files 210 | GeneratedArtifacts/ 211 | _Pvt_Extensions/ 212 | ModelManifest.xml 213 | -------------------------------------------------------------------------------- /FormatWith.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.12 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B4638D33-4E45-4FB4-A119-91F90F876E8D}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A15B581D-78A5-4783-870C-FB89A1A09FD5}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FormatWith", "FormatWith\FormatWith.csproj", "{921D7EE8-732B-4237-9020-C97C8ECFF78B}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FormatWithTests", "FormatWithTests\FormatWithTests.csproj", "{3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A}" 13 | EndProject 14 | Global 15 | GlobalSection(Performance) = preSolution 16 | HasPerformanceSessions = true 17 | EndGlobalSection 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Debug|ARM = Debug|ARM 21 | Debug|x64 = Debug|x64 22 | Debug|x86 = Debug|x86 23 | Release|Any CPU = Release|Any CPU 24 | Release|ARM = Release|ARM 25 | Release|x64 = Release|x64 26 | Release|x86 = Release|x86 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {921D7EE8-732B-4237-9020-C97C8ECFF78B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {921D7EE8-732B-4237-9020-C97C8ECFF78B}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {921D7EE8-732B-4237-9020-C97C8ECFF78B}.Debug|ARM.ActiveCfg = Debug|Any CPU 32 | {921D7EE8-732B-4237-9020-C97C8ECFF78B}.Debug|ARM.Build.0 = Debug|Any CPU 33 | {921D7EE8-732B-4237-9020-C97C8ECFF78B}.Debug|x64.ActiveCfg = Debug|Any CPU 34 | {921D7EE8-732B-4237-9020-C97C8ECFF78B}.Debug|x64.Build.0 = Debug|Any CPU 35 | {921D7EE8-732B-4237-9020-C97C8ECFF78B}.Debug|x86.ActiveCfg = Debug|Any CPU 36 | {921D7EE8-732B-4237-9020-C97C8ECFF78B}.Debug|x86.Build.0 = Debug|Any CPU 37 | {921D7EE8-732B-4237-9020-C97C8ECFF78B}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {921D7EE8-732B-4237-9020-C97C8ECFF78B}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {921D7EE8-732B-4237-9020-C97C8ECFF78B}.Release|ARM.ActiveCfg = Release|Any CPU 40 | {921D7EE8-732B-4237-9020-C97C8ECFF78B}.Release|ARM.Build.0 = Release|Any CPU 41 | {921D7EE8-732B-4237-9020-C97C8ECFF78B}.Release|x64.ActiveCfg = Release|Any CPU 42 | {921D7EE8-732B-4237-9020-C97C8ECFF78B}.Release|x64.Build.0 = Release|Any CPU 43 | {921D7EE8-732B-4237-9020-C97C8ECFF78B}.Release|x86.ActiveCfg = Release|Any CPU 44 | {921D7EE8-732B-4237-9020-C97C8ECFF78B}.Release|x86.Build.0 = Release|Any CPU 45 | {3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A}.Debug|ARM.ActiveCfg = Debug|Any CPU 48 | {3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A}.Debug|ARM.Build.0 = Debug|Any CPU 49 | {3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A}.Debug|x64.ActiveCfg = Debug|Any CPU 50 | {3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A}.Debug|x64.Build.0 = Debug|Any CPU 51 | {3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A}.Debug|x86.ActiveCfg = Debug|Any CPU 52 | {3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A}.Debug|x86.Build.0 = Debug|Any CPU 53 | {3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A}.Release|ARM.ActiveCfg = Release|Any CPU 56 | {3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A}.Release|ARM.Build.0 = Release|Any CPU 57 | {3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A}.Release|x64.ActiveCfg = Release|Any CPU 58 | {3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A}.Release|x64.Build.0 = Release|Any CPU 59 | {3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A}.Release|x86.ActiveCfg = Release|Any CPU 60 | {3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A}.Release|x86.Build.0 = Release|Any CPU 61 | {06B0DD6E-D6C3-4F67-BFCA-C7E6660C2D04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 62 | {06B0DD6E-D6C3-4F67-BFCA-C7E6660C2D04}.Debug|Any CPU.Build.0 = Debug|Any CPU 63 | {06B0DD6E-D6C3-4F67-BFCA-C7E6660C2D04}.Debug|ARM.ActiveCfg = Debug|Any CPU 64 | {06B0DD6E-D6C3-4F67-BFCA-C7E6660C2D04}.Debug|ARM.Build.0 = Debug|Any CPU 65 | {06B0DD6E-D6C3-4F67-BFCA-C7E6660C2D04}.Debug|x64.ActiveCfg = Debug|Any CPU 66 | {06B0DD6E-D6C3-4F67-BFCA-C7E6660C2D04}.Debug|x64.Build.0 = Debug|Any CPU 67 | {06B0DD6E-D6C3-4F67-BFCA-C7E6660C2D04}.Debug|x86.ActiveCfg = Debug|Any CPU 68 | {06B0DD6E-D6C3-4F67-BFCA-C7E6660C2D04}.Debug|x86.Build.0 = Debug|Any CPU 69 | {06B0DD6E-D6C3-4F67-BFCA-C7E6660C2D04}.Release|Any CPU.ActiveCfg = Release|Any CPU 70 | {06B0DD6E-D6C3-4F67-BFCA-C7E6660C2D04}.Release|Any CPU.Build.0 = Release|Any CPU 71 | {06B0DD6E-D6C3-4F67-BFCA-C7E6660C2D04}.Release|ARM.ActiveCfg = Release|Any CPU 72 | {06B0DD6E-D6C3-4F67-BFCA-C7E6660C2D04}.Release|ARM.Build.0 = Release|Any CPU 73 | {06B0DD6E-D6C3-4F67-BFCA-C7E6660C2D04}.Release|x64.ActiveCfg = Release|Any CPU 74 | {06B0DD6E-D6C3-4F67-BFCA-C7E6660C2D04}.Release|x64.Build.0 = Release|Any CPU 75 | {06B0DD6E-D6C3-4F67-BFCA-C7E6660C2D04}.Release|x86.ActiveCfg = Release|Any CPU 76 | {06B0DD6E-D6C3-4F67-BFCA-C7E6660C2D04}.Release|x86.Build.0 = Release|Any CPU 77 | EndGlobalSection 78 | GlobalSection(SolutionProperties) = preSolution 79 | HideSolutionNode = FALSE 80 | EndGlobalSection 81 | GlobalSection(NestedProjects) = preSolution 82 | {921D7EE8-732B-4237-9020-C97C8ECFF78B} = {B4638D33-4E45-4FB4-A119-91F90F876E8D} 83 | {3F2936F4-0E9D-43B5-A7CE-1A22DF9F9A8A} = {B4638D33-4E45-4FB4-A119-91F90F876E8D} 84 | EndGlobalSection 85 | GlobalSection(ExtensibilityGlobals) = postSolution 86 | SolutionGuid = {CAB2E4E0-1D18-4C27-9022-229AAB03C637} 87 | EndGlobalSection 88 | GlobalSection(Performance) = preSolution 89 | HasPerformanceSessions = true 90 | EndGlobalSection 91 | EndGlobal 92 | -------------------------------------------------------------------------------- /FormatWith/FormatWith-Release.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crozone/FormatWith/35671b04415185341517915345b0783bbf162073/FormatWith/FormatWith-Release.snk -------------------------------------------------------------------------------- /FormatWith/FormatWith.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | FormatWith 6 | FormatWith 7 | String extension methods for performing {named} {{parameterized}} string formatting, written for NetStandard 2.0 8 | FormatWith 9 | FormatWith 10 | FormatWith 11 | 3.0.1 12 | Ryan Crosby 13 | Copyright © Ryan Crosby 2017 - 2020 14 | named string formatter extension NetStandard 2.0 15 | https://github.com/crozone/FormatWith 16 | MIT 17 | true 18 | false 19 | false 20 | false 21 | false 22 | false 23 | false 24 | true 25 | https://github.com/crozone/FormatWith 26 | git 27 | 28 | Key:format syntax now supported for FormatWith and FormattableWith. 29 | Add strong signing. 30 | 31 | true 32 | FormatWith-Release.snk 33 | false 34 | 35 | 36 | 37 | bin\Debug\netstandard2.0\FormatWith.xml 38 | 39 | 40 | 41 | bin\Release\netstandard2.0\FormatWith.xml 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /FormatWith/Internal/FormatHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | 7 | namespace FormatWith.Internal 8 | { 9 | /// 10 | /// Contains all string processing and tokenizing methods for FormatWith 11 | /// 12 | internal static class FormatHelpers 13 | { 14 | /// 15 | /// Processes a list of format tokens into a string 16 | /// 17 | /// List of tokens to turn into a string 18 | /// The function used to perform the replacements on the format tokens 19 | /// The behaviour to use when the format string contains a parameter that is not present in the lookup dictionary 20 | /// When the is specified, this string is used as a fallback replacement value when the parameter is present in the lookup dictionary. 21 | /// Provides a hint to the underlying string builder to help reduce buffer reallocations. 22 | /// The processed result of joining the tokens with the replacement dictionary. 23 | public static string ProcessTokens( 24 | IEnumerable tokens, 25 | Func handler, 26 | MissingKeyBehaviour missingKeyBehaviour, 27 | object fallbackReplacementValue, 28 | int outputLengthHint) 29 | { 30 | // create a StringBuilder to hold the resultant output string 31 | // use the input hint as the initial size 32 | StringBuilder resultBuilder = new StringBuilder(outputLengthHint); 33 | 34 | foreach (FormatToken thisToken in tokens) 35 | { 36 | if (thisToken.TokenType == TokenType.Text) 37 | { 38 | // token is a text token 39 | // add the token to the result string builder 40 | resultBuilder.Append(thisToken.SourceString, thisToken.StartIndex, thisToken.Length); 41 | } 42 | else if (thisToken.TokenType == TokenType.Parameter) 43 | { 44 | // token is a parameter token 45 | // perform parameter logic now. 46 | var tokenKey = thisToken.Value; 47 | string format = null; 48 | var separatorIdx = tokenKey.IndexOf(":", StringComparison.Ordinal); 49 | if (separatorIdx > -1) 50 | { 51 | tokenKey = thisToken.Value.Substring(0, separatorIdx); 52 | format = thisToken.Value.Substring(separatorIdx + 1); 53 | } 54 | 55 | // append the replacement for this parameter 56 | ReplacementResult replacementResult = handler(tokenKey, format); 57 | 58 | if (replacementResult.Success) 59 | { 60 | // the key exists, add the replacement value 61 | // this does nothing if replacement value is null 62 | if (string.IsNullOrWhiteSpace(format)) 63 | { 64 | resultBuilder.Append(replacementResult.Value); 65 | } 66 | else 67 | { 68 | resultBuilder.AppendFormat("{0:" + format + "}", replacementResult.Value); 69 | } 70 | } 71 | else 72 | { 73 | // the key does not exist, handle this using the missing key behaviour specified. 74 | switch (missingKeyBehaviour) 75 | { 76 | case MissingKeyBehaviour.ThrowException: 77 | // the key was not found as a possible replacement, throw exception 78 | throw new KeyNotFoundException($"The parameter \"{thisToken.Value}\" was not present in the lookup dictionary"); 79 | case MissingKeyBehaviour.ReplaceWithFallback: 80 | resultBuilder.Append(fallbackReplacementValue); 81 | break; 82 | case MissingKeyBehaviour.Ignore: 83 | // the replacement value is the input key as a parameter. 84 | // use source string and start/length directly with append rather than 85 | // parameter.ParameterKey to avoid allocating an extra string 86 | resultBuilder.Append(thisToken.SourceString, thisToken.StartIndex, thisToken.Length); 87 | break; 88 | } 89 | } 90 | } 91 | } 92 | 93 | // return the resultant string 94 | return resultBuilder.ToString(); 95 | } 96 | 97 | /// 98 | /// Processes a list of format tokens into a string 99 | /// 100 | /// List of tokens to turn into a string 101 | /// The function used to perform the replacements on the format tokens 102 | /// The behaviour to use when the format string contains a parameter that is not present in the lookup dictionary 103 | /// When the is specified, this string is used as a fallback replacement value when the parameter is present in the lookup dictionary. 104 | /// 105 | /// The processed result of joining the tokens with the replacement dictionary. 106 | public static FormattableString ProcessTokensIntoFormattableString( 107 | IEnumerable tokens, 108 | Func handler, 109 | MissingKeyBehaviour missingKeyBehaviour, 110 | object fallbackReplacementValue, 111 | int outputLengthHint) 112 | { 113 | List replacementParams = new List(); 114 | 115 | // create a StringBuilder to hold the resultant output string 116 | // use the input hint as the initial size 117 | StringBuilder resultBuilder = new StringBuilder(outputLengthHint); 118 | 119 | // this is the index of the current placeholder in the composite format string 120 | int placeholderIndex = 0; 121 | 122 | foreach (FormatToken thisToken in tokens) 123 | { 124 | if (thisToken.TokenType == TokenType.Text) 125 | { 126 | // token is a text token. 127 | // add the token to the result string builder. 128 | // because this text is going into a standard composite format string, 129 | // any instaces of { or } must be escaped with {{ and }} 130 | resultBuilder.AppendWithEscapedBrackets(thisToken.SourceString, thisToken.StartIndex, thisToken.Length); 131 | } 132 | else if (thisToken.TokenType == TokenType.Parameter) 133 | { 134 | // token is a parameter token 135 | // perform parameter logic now. 136 | var tokenKey = thisToken.Value; 137 | string format = null; 138 | var separatorIdx = tokenKey.IndexOf(":", StringComparison.Ordinal); 139 | if (separatorIdx > -1) 140 | { 141 | tokenKey = thisToken.Value.Substring(0, separatorIdx); 142 | format = thisToken.Value.Substring(separatorIdx + 1); 143 | } 144 | 145 | // append the replacement for this parameter 146 | ReplacementResult replacementResult = handler(tokenKey); 147 | 148 | string IndexAndFormat() 149 | { 150 | if (string.IsNullOrWhiteSpace(format)) 151 | { 152 | return "{" + placeholderIndex + "}"; 153 | } 154 | 155 | return "{" + placeholderIndex + ":" + format + "}"; 156 | } 157 | 158 | // append the replacement for this parameter 159 | if (replacementResult.Success) 160 | { 161 | // Instead of appending the replacement value directly as before, 162 | // append the next placeholder with the current placeholder index. 163 | // Add the actual replacement format item into the replacement values. 164 | resultBuilder.Append(IndexAndFormat()); 165 | placeholderIndex++; 166 | replacementParams.Add(replacementResult.Value); 167 | } 168 | else 169 | { 170 | // the key does not exist, handle this using the missing key behaviour specified. 171 | switch (missingKeyBehaviour) 172 | { 173 | case MissingKeyBehaviour.ThrowException: 174 | // the key was not found as a possible replacement, throw exception 175 | throw new KeyNotFoundException($"The parameter \"{thisToken.Value}\" was not present in the lookup dictionary"); 176 | case MissingKeyBehaviour.ReplaceWithFallback: 177 | // Instead of appending the replacement value directly as before, 178 | // append the next placeholder with the current placeholder index. 179 | // Add the actual replacement format item into the replacement values. 180 | resultBuilder.Append(IndexAndFormat()); 181 | placeholderIndex++; 182 | replacementParams.Add(fallbackReplacementValue); 183 | break; 184 | case MissingKeyBehaviour.Ignore: 185 | resultBuilder.AppendWithEscapedBrackets(thisToken.SourceString, thisToken.StartIndex, thisToken.Length); 186 | break; 187 | } 188 | } 189 | } 190 | } 191 | 192 | // return the resultant string 193 | return FormattableStringFactory.Create(resultBuilder.ToString(), replacementParams.ToArray()); 194 | } 195 | 196 | /// 197 | /// Tokenizes a named format string into a list of text and parameter tokens for later processing. 198 | /// 199 | /// The format string, containing keys like {foo} 200 | /// The character used to begin parameters 201 | /// The character used to end parameters 202 | /// A list of text and parameter tokens representing the input format string 203 | public static IEnumerable Tokenize(string formatString, char openBraceChar = '{', char closeBraceChar = '}') 204 | { 205 | if (formatString == null) throw new ArgumentNullException($"{nameof(formatString)} cannot be null."); 206 | 207 | int currentTokenStart = 0; 208 | 209 | // start the state machine! 210 | 211 | bool insideBraces = false; 212 | 213 | int index = 0; 214 | while (index < formatString.Length) 215 | { 216 | if (!insideBraces) 217 | { 218 | // currently not inside a pair of braces in the format string 219 | if (formatString[index] == openBraceChar) 220 | { 221 | // check if the brace is escaped 222 | if (index < formatString.Length - 1 && formatString[index + 1] == openBraceChar) 223 | { 224 | // ESCAPED OPEN BRACE 225 | 226 | // we have hit an escaped open brace 227 | // return current normal text, as well as the first brace 228 | // implemented as yield return, this generates a IEnumerator state machine. 229 | yield return new FormatToken(TokenType.Text, formatString, currentTokenStart, (index - currentTokenStart) + 1); 230 | 231 | // skip over braces 232 | index += 2; 233 | 234 | // set new current token start and current token length 235 | currentTokenStart = index; 236 | 237 | continue; 238 | } 239 | else 240 | { 241 | // START OF PARAMETER 242 | 243 | // not an escaped brace, set state to inside brace 244 | insideBraces = true; 245 | 246 | // we are leaving standard text and entering into a parameter 247 | // add the text traversed so far as a text token 248 | if (currentTokenStart < index) 249 | { 250 | yield return new FormatToken(TokenType.Text, formatString, currentTokenStart, (index - currentTokenStart)); 251 | } 252 | 253 | // set the start index of the token to the start of this parameter 254 | currentTokenStart = index; 255 | 256 | index++; 257 | 258 | continue; 259 | } 260 | } 261 | else if (formatString[index] == closeBraceChar) 262 | { 263 | // handle case where closing brace is encountered outside braces 264 | if (index < formatString.Length - 1 && formatString[index + 1] == closeBraceChar) 265 | { 266 | // this is an escaped closing brace, this is okay 267 | 268 | // add the current normal text, as well as the first brace, to the 269 | // list of tokens as a text token. 270 | yield return new FormatToken(TokenType.Text, formatString, currentTokenStart, (index - currentTokenStart) + 1); 271 | 272 | // skip over braces 273 | index += 2; 274 | 275 | // set new current token start and current token length 276 | currentTokenStart = index; 277 | 278 | continue; 279 | } 280 | else 281 | { 282 | // this is an unescaped closing brace outside of braces. 283 | // throw a format exception 284 | throw new FormatException($"Unexpected closing brace at position {index}"); 285 | } 286 | } 287 | else 288 | { 289 | // move onto next character 290 | index++; 291 | continue; 292 | } 293 | } 294 | else 295 | { 296 | // currently inside a pair of braces in the format string 297 | if (formatString[index] == openBraceChar) 298 | { 299 | // found an opening brace 300 | // check if the brace is escaped 301 | if (index < formatString.Length - 1 && formatString[index + 1] == openBraceChar) 302 | { 303 | // there are escaped braces within the key 304 | // this is illegal, throw a format exception 305 | throw new FormatException($"Illegal escaped opening braces within a parameter at position {index}"); 306 | } 307 | else 308 | { 309 | // not an escaped brace, we have an unexpected opening brace within a pair of braces 310 | throw new FormatException($"Unexpected opening brace inside a parameter at position {index}"); 311 | } 312 | } 313 | else if (formatString[index] == closeBraceChar) 314 | { 315 | // END OF PARAMETER 316 | // handle case where closing brace is encountered inside braces 317 | // don't attempt to check for escaped braces here - always assume the first brace closes the braces 318 | // since we cannot have escaped braces within parameters. 319 | 320 | // Add the parameter information to the parameter list 321 | yield return new FormatToken(TokenType.Parameter, formatString, currentTokenStart, (index - currentTokenStart) + 1); 322 | 323 | // set the state to be outside of any braces 324 | insideBraces = false; 325 | 326 | // jump over brace 327 | index++; 328 | 329 | // update current token start 330 | currentTokenStart = index; 331 | 332 | // jump to next state 333 | continue; 334 | } // if } 335 | else 336 | { 337 | // character has no special meaning, it is part of the current key 338 | // move onto next character 339 | index++; 340 | continue; 341 | } // else 342 | } // if inside brace 343 | } // while index < formatString.Length 344 | 345 | // after the loop, if all braces were balanced, we should be outside all braces 346 | // if we're not, the input string was misformatted. 347 | if (insideBraces) 348 | { 349 | throw new FormatException($"The format string ended before the parameter was closed. Position {index}"); 350 | } 351 | else 352 | { 353 | // outside braces. Add on any remaining text at the end of the format string 354 | if (currentTokenStart < index) 355 | { 356 | yield return new FormatToken(TokenType.Text, formatString, currentTokenStart, index - currentTokenStart); 357 | } 358 | } 359 | 360 | // finished tokenizing, so yield break to make MoveNext return false on the IEnumerator 361 | yield break; 362 | } 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /FormatWith/Internal/FormatToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace FormatWith.Internal 6 | { 7 | internal struct FormatToken 8 | { 9 | public FormatToken(TokenType tokenType, string source, int startIndex, int length) 10 | { 11 | TokenType = tokenType; 12 | SourceString = source; 13 | StartIndex = startIndex; 14 | Length = length; 15 | } 16 | 17 | public TokenType TokenType { get; } 18 | 19 | /// 20 | /// The source format string that the token exists within 21 | /// 22 | public string SourceString { get; } 23 | 24 | /// 25 | /// The index of the start of the whole token, relative to the start of the source format string. 26 | /// 27 | public int StartIndex { get; } 28 | 29 | /// 30 | /// The length of the whole token. 31 | /// 32 | public int Length { get; } 33 | 34 | /// 35 | /// Gets the complete value. 36 | /// This performs a substring operation and allocates a new string object. 37 | /// 38 | public string Raw { 39 | get { 40 | return SourceString.Substring(StartIndex, Length); 41 | } 42 | } 43 | 44 | /// 45 | /// Gets the token inner text. 46 | /// This performs a substring operation and allocates a new string object. 47 | /// 48 | public string Value { 49 | get { 50 | if (TokenType == TokenType.Parameter) 51 | { 52 | return SourceString.Substring(StartIndex + 1, Length - 2); 53 | } 54 | else 55 | { 56 | return SourceString.Substring(StartIndex, Length); 57 | } 58 | } 59 | } 60 | } 61 | 62 | internal enum TokenType 63 | { 64 | Parameter, 65 | Text 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /FormatWith/Internal/FormatWithMethods.cs: -------------------------------------------------------------------------------- 1 | using FormatWith.Internal; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | 8 | namespace FormatWith.Internal 9 | { 10 | internal static class FormatWithMethods 11 | { 12 | public static string FormatWith( 13 | string formatString, 14 | IDictionary replacements, 15 | MissingKeyBehaviour missingKeyBehaviour = MissingKeyBehaviour.ThrowException, 16 | string fallbackReplacementValue = null, 17 | char openBraceChar = '{', 18 | char closeBraceChar = '}') 19 | { 20 | return FormatWith( 21 | formatString, 22 | (key, format) => new ReplacementResult(replacements.TryGetValue(key, out string value), value), 23 | missingKeyBehaviour, 24 | fallbackReplacementValue, 25 | openBraceChar, 26 | closeBraceChar); 27 | } 28 | 29 | public static string FormatWith( 30 | string formatString, 31 | IDictionary replacements, 32 | MissingKeyBehaviour missingKeyBehaviour = MissingKeyBehaviour.ThrowException, 33 | object fallbackReplacementValue = null, 34 | char openBraceChar = '{', 35 | char closeBraceChar = '}') 36 | { 37 | return FormatWith( 38 | formatString, 39 | (key, format) => new ReplacementResult(replacements.TryGetValue(key, out object value), value), 40 | missingKeyBehaviour, 41 | fallbackReplacementValue, 42 | openBraceChar, 43 | closeBraceChar); 44 | } 45 | 46 | private static BindingFlags propertyBindingFlags = BindingFlags.Instance | BindingFlags.Public; 47 | 48 | public static string FormatWith( 49 | string formatString, 50 | object replacementObject, 51 | MissingKeyBehaviour missingKeyBehaviour = MissingKeyBehaviour.ThrowException, 52 | object fallbackReplacementValue = null, 53 | char openBraceChar = '{', 54 | char closeBraceChar = '}') 55 | { 56 | if (replacementObject == null) throw new ArgumentNullException(nameof(replacementObject)); 57 | 58 | return FormatWith(formatString, 59 | (key, format) => FromReplacementObject(key, replacementObject), 60 | missingKeyBehaviour, 61 | fallbackReplacementValue, 62 | openBraceChar, 63 | closeBraceChar); 64 | } 65 | 66 | public static string FormatWith( 67 | string formatString, 68 | Func handler, 69 | MissingKeyBehaviour missingKeyBehaviour = MissingKeyBehaviour.ThrowException, 70 | object fallbackReplacementValue = null, 71 | char openBraceChar = '{', 72 | char closeBraceChar = '}') 73 | { 74 | if (formatString.Length == 0) return string.Empty; 75 | 76 | // get the parameters from the format string 77 | IEnumerable tokens = FormatHelpers.Tokenize(formatString, openBraceChar, closeBraceChar); 78 | return FormatHelpers.ProcessTokens(tokens, handler, missingKeyBehaviour, fallbackReplacementValue, formatString.Length * 2); 79 | } 80 | 81 | public static FormattableString FormattableWith( 82 | string formatString, 83 | IDictionary replacements, 84 | MissingKeyBehaviour missingKeyBehaviour = MissingKeyBehaviour.ThrowException, 85 | string fallbackReplacementValue = null, 86 | char openBraceChar = '{', 87 | char closeBraceChar = '}') 88 | { 89 | return FormattableWith( 90 | formatString, 91 | key => new ReplacementResult(replacements.TryGetValue(key, out string value), value), 92 | missingKeyBehaviour, 93 | fallbackReplacementValue, 94 | openBraceChar, 95 | closeBraceChar); 96 | } 97 | 98 | public static FormattableString FormattableWith( 99 | string formatString, 100 | IDictionary replacements, 101 | MissingKeyBehaviour missingKeyBehaviour = MissingKeyBehaviour.ThrowException, 102 | object fallbackReplacementValue = null, 103 | char openBraceChar = '{', 104 | char closeBraceChar = '}') 105 | { 106 | return FormattableWith( 107 | formatString, 108 | key => new ReplacementResult(replacements.TryGetValue(key, out object value), value), 109 | missingKeyBehaviour, 110 | fallbackReplacementValue, 111 | openBraceChar, 112 | closeBraceChar); 113 | } 114 | 115 | public static FormattableString FormattableWith( 116 | string formatString, 117 | object replacementObject, 118 | MissingKeyBehaviour missingKeyBehaviour = MissingKeyBehaviour.ThrowException, 119 | object fallbackReplacementValue = null, 120 | char openBraceChar = '{', 121 | char closeBraceChar = '}') 122 | { 123 | return FormattableWith(formatString, 124 | key => FromReplacementObject(key, replacementObject), 125 | missingKeyBehaviour, 126 | fallbackReplacementValue, 127 | openBraceChar, 128 | closeBraceChar); 129 | } 130 | 131 | public static FormattableString FormattableWith( 132 | string formatString, 133 | Func handler, 134 | MissingKeyBehaviour missingKeyBehaviour = MissingKeyBehaviour.ThrowException, 135 | object fallbackReplacementValue = null, 136 | char openBraceChar = '{', 137 | char closeBraceChar = '}') 138 | { 139 | // get the parameters from the format string 140 | IEnumerable tokens = FormatHelpers.Tokenize(formatString, openBraceChar, closeBraceChar); 141 | return FormatHelpers.ProcessTokensIntoFormattableString(tokens, handler, missingKeyBehaviour, fallbackReplacementValue, formatString.Length * 2); 142 | } 143 | 144 | /// 145 | /// Gets an IEnumerable<string> that will return all format parameters used within the format string. 146 | /// 147 | /// The format string to be parsed 148 | /// The character used to begin parameters 149 | /// The character used to end parameters 150 | /// 151 | public static IEnumerable GetFormatParameters( 152 | string formatString, 153 | char openBraceChar = '{', 154 | char closeBraceChar = '}') 155 | { 156 | return FormatHelpers.Tokenize(formatString, openBraceChar, closeBraceChar) 157 | .Where(t => t.TokenType == TokenType.Parameter) 158 | .Select(pt => pt.Value); 159 | } 160 | 161 | private static ReplacementResult FromReplacementObject(string key, object replacementObject) 162 | { 163 | // need to split this into accessors so we can traverse nested objects 164 | var members = key.Split(new[] { "." }, StringSplitOptions.None); 165 | if (members.Length == 1) 166 | { 167 | PropertyInfo propertyInfo = replacementObject.GetType().GetProperty(key, propertyBindingFlags); 168 | 169 | if (propertyInfo == null) 170 | { 171 | return new ReplacementResult(false, null); 172 | } 173 | else 174 | { 175 | return new ReplacementResult(true, propertyInfo.GetValue(replacementObject)); 176 | } 177 | } 178 | else 179 | { 180 | object currentObject = replacementObject; 181 | 182 | foreach (var member in members) 183 | { 184 | PropertyInfo propertyInfo = currentObject.GetType().GetProperty(member, propertyBindingFlags); 185 | 186 | if (propertyInfo == null) 187 | { 188 | return new ReplacementResult(false, null); 189 | } 190 | else 191 | { 192 | currentObject = propertyInfo.GetValue(currentObject); 193 | } 194 | } 195 | 196 | return new ReplacementResult(true, currentObject); 197 | } 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /FormatWith/Internal/StringBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace FormatWith.Internal 6 | { 7 | internal static class StringBuilderExtensions 8 | { 9 | public static void AppendWithEscapedBrackets( 10 | this StringBuilder stringBuilder, 11 | string value, 12 | int startIndex, 13 | int count, 14 | char openBraceChar = '{', 15 | char closeBraceChar = '}') 16 | { 17 | int currentTokenStart = startIndex; 18 | for (int i = startIndex; i < startIndex + count; i++) 19 | { 20 | if (value[i] == openBraceChar) 21 | { 22 | stringBuilder.Append(value, currentTokenStart, i - currentTokenStart); 23 | stringBuilder.Append(openBraceChar); 24 | currentTokenStart = i; 25 | } 26 | else if (value[i] == closeBraceChar) 27 | { 28 | stringBuilder.Append(value, currentTokenStart, i - currentTokenStart); 29 | stringBuilder.Append(closeBraceChar); 30 | currentTokenStart = i; 31 | } 32 | } 33 | 34 | // add the final section 35 | stringBuilder.Append(value, currentTokenStart, (startIndex + count) - currentTokenStart); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /FormatWith/MissingKeyBehaviour.cs: -------------------------------------------------------------------------------- 1 | namespace FormatWith 2 | { 3 | /// 4 | /// Behaviour to use when a parameter is given that has no key in the replacement dictionary 5 | /// 6 | public enum MissingKeyBehaviour 7 | { 8 | /// 9 | /// Throws a FormatException 10 | /// 11 | ThrowException, 12 | /// 13 | /// Replaces the parameter with a given fallback string 14 | /// 15 | ReplaceWithFallback, 16 | /// 17 | /// Ignores the parameter, leaving it unprocessed in the output string 18 | /// 19 | Ignore 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /FormatWith/ReplacementResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace FormatWith 6 | { 7 | /// 8 | /// Represents the result of a substitution for a parameter within a format string. 9 | /// 10 | public struct ReplacementResult 11 | { 12 | /// 13 | /// Represents the result of a substitution for a parameter within a format string. 14 | /// 15 | /// Represents whether or not the substitution was successful. 16 | /// The new value for the substituted format parameter. 17 | public ReplacementResult(bool success, object value) 18 | { 19 | Success = success; 20 | Value = value; 21 | } 22 | 23 | /// 24 | /// Represents whether or not the substitution was successful. 25 | /// If true, the handler was successfully able to replace this parameter with the substituted value. 26 | /// If false, the substitution failed, and Value will be set to null. 27 | /// 28 | public bool Success { get; } 29 | 30 | /// 31 | /// The new value for the substituted format parameter. 32 | /// If Success is false, this should be set to null. 33 | /// 34 | public object Value { get; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /FormatWith/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FormatWith.Internal; 4 | using System.Collections; 5 | 6 | namespace FormatWith 7 | { 8 | /// 9 | /// The string extensions provided by FormatWith for string formatting. 10 | /// 11 | public static class StringExtensions 12 | { 13 | #region FormatWith Overloads 14 | /// 15 | /// Formats a string with the values given by the properties on an input object. 16 | /// 17 | /// The format string, containing keys like {foo} 18 | /// The object whose properties should be injected in the string 19 | /// The formatted string 20 | public static string FormatWith( 21 | this string formatString, 22 | object replacementObject) 23 | { 24 | // wrap the type object in a wrapper Dictionary class that exposes the properties as dictionary keys via reflection 25 | return FormatWithMethods.FormatWith(formatString, replacementObject); 26 | } 27 | 28 | /// 29 | /// Formats a string with the values given by the properties on an input object. 30 | /// 31 | /// The format string, containing keys like {foo} 32 | /// The object whose properties should be injected in the string 33 | /// The behaviour to use when the format string contains a parameter that is not present in the lookup dictionary 34 | /// When the is specified, this string is used as a fallback replacement value when the parameter is present in the lookup dictionary. 35 | /// The character used to begin parameters 36 | /// The character used to end parameters 37 | /// The formatted string 38 | public static string FormatWith( 39 | this string formatString, 40 | object replacementObject, 41 | MissingKeyBehaviour missingKeyBehaviour = MissingKeyBehaviour.ThrowException, 42 | object fallbackReplacementValue = null, 43 | char openBraceChar = '{', 44 | char closeBraceChar = '}') 45 | { 46 | // wrap the type object in a wrapper Dictionary class that exposes the properties as dictionary keys via reflection 47 | return FormatWithMethods.FormatWith( 48 | formatString, 49 | replacementObject, 50 | missingKeyBehaviour, 51 | fallbackReplacementValue, 52 | openBraceChar, 53 | closeBraceChar); 54 | } 55 | 56 | /// 57 | /// Formats a string with the values of the dictionary. 58 | /// 59 | /// The format string, containing keys like {foo} 60 | /// An with keys and values to inject into the string 61 | /// The formatted string 62 | public static string FormatWith( 63 | this string formatString, 64 | IDictionary replacements) 65 | { 66 | // wrap the IDictionary in a wrapper Dictionary class that casts the values to objects as needed 67 | return FormatWithMethods.FormatWith(formatString, replacements); 68 | } 69 | 70 | /// 71 | /// Formats a string with the values of the dictionary. 72 | /// 73 | /// The format string, containing keys like {foo} 74 | /// An with keys and values to inject into the string 75 | /// The behaviour to use when the format string contains a parameter that is not present in the lookup dictionary 76 | /// When the is specified, this string is used as a fallback replacement value when the parameter is present in the lookup dictionary. 77 | /// The character used to begin parameters 78 | /// The character used to end parameters 79 | /// The formatted string 80 | public static string FormatWith( 81 | this string formatString, 82 | IDictionary replacements, 83 | MissingKeyBehaviour missingKeyBehaviour = MissingKeyBehaviour.ThrowException, 84 | string fallbackReplacementValue = null, 85 | char openBraceChar = '{', 86 | char closeBraceChar = '}') 87 | { 88 | // wrap the IDictionary in a wrapper Dictionary class that casts the values to objects as needed 89 | return FormatWithMethods.FormatWith( 90 | formatString, 91 | replacements, 92 | missingKeyBehaviour, 93 | fallbackReplacementValue, 94 | openBraceChar, 95 | closeBraceChar); 96 | } 97 | 98 | /// 99 | /// Formats a string with the values of the dictionary. 100 | /// The string representation of each object value in the dictionary is used as the format parameter. 101 | /// 102 | /// The format string, containing keys like {foo} 103 | /// An with keys and values to inject into the string 104 | /// The formatted string 105 | public static string FormatWith(this string formatString, IDictionary replacements) 106 | { 107 | return FormatWithMethods.FormatWith(formatString, replacements); 108 | } 109 | 110 | /// 111 | /// Formats a string with the values of the dictionary. 112 | /// The string representation of each object value in the dictionary is used as the format parameter. 113 | /// 114 | /// The format string, containing keys like {foo} 115 | /// An with keys and values to inject into the string 116 | /// The behaviour to use when the format string contains a parameter that is not present in the lookup dictionary 117 | /// When the is specified, this string is used as a fallback replacement value when the parameter is present in the lookup dictionary. 118 | /// The character used to begin parameters 119 | /// The character used to end parameters 120 | /// The formatted string 121 | public static string FormatWith( 122 | this string formatString, 123 | IDictionary replacements, 124 | MissingKeyBehaviour missingKeyBehaviour = MissingKeyBehaviour.ThrowException, 125 | object fallbackReplacementValue = null, 126 | char openBraceChar = '{', 127 | char closeBraceChar = '}') 128 | { 129 | return FormatWithMethods.FormatWith( 130 | formatString, 131 | replacements, 132 | missingKeyBehaviour, 133 | fallbackReplacementValue, 134 | openBraceChar, 135 | closeBraceChar); 136 | } 137 | 138 | /// 139 | /// Formats a string, using a handler function to provide the value 140 | /// of each parameter. 141 | /// 142 | /// The format string, containing keys like {foo} 143 | /// A handler function that transforms each parameter into a 144 | /// The formatted string 145 | public static string FormatWith( 146 | this string formatString, 147 | Func handler) 148 | { 149 | return FormatWithMethods.FormatWith(formatString, handler); 150 | } 151 | 152 | /// 153 | /// Formats a string, using a handler function to provide the value 154 | /// of each parameter. 155 | /// 156 | /// The format string, containing keys like {foo} 157 | /// A handler function that transforms each parameter into a 158 | /// The behaviour to use when the format string contains a parameter that cannot be replaced by the handler 159 | /// When the is specified, this object is used as a fallback replacement value. 160 | /// The character used to begin parameters 161 | /// The character used to end parameters 162 | /// The formatted string 163 | public static string FormatWith( 164 | this string formatString, 165 | Func handler, 166 | MissingKeyBehaviour missingKeyBehaviour = MissingKeyBehaviour.ThrowException, 167 | object fallbackReplacementValue = null, 168 | char openBraceChar = '{', 169 | char closeBraceChar = '}') 170 | { 171 | return FormatWithMethods.FormatWith( 172 | formatString, 173 | handler, 174 | missingKeyBehaviour, 175 | fallbackReplacementValue, 176 | openBraceChar, 177 | closeBraceChar); 178 | } 179 | 180 | #endregion 181 | 182 | #region FormattableWith Overloads 183 | /// 184 | /// Produces a representing the input format string. 185 | /// 186 | /// The format string, containing keys like {foo} 187 | /// The object whose properties should be injected in the string 188 | /// The resultant 189 | public static FormattableString FormattableWith(this string formatString, object replacementObject) 190 | { 191 | // wrap the type object in a wrapper Dictionary class that exposes the properties as dictionary keys via reflection 192 | return FormatWithMethods.FormattableWith(formatString, replacementObject); 193 | } 194 | 195 | /// 196 | /// Produces a representing the input format string. 197 | /// 198 | /// The format string, containing keys like {foo} 199 | /// The object whose properties should be injected in the string 200 | /// The behaviour to use when the format string contains a parameter that is not present in the lookup dictionary 201 | /// When the is specified, this string is used as a fallback replacement value when the parameter is present in the lookup dictionary. 202 | /// The character used to begin parameters 203 | /// The character used to end parameters 204 | /// The resultant 205 | public static FormattableString FormattableWith( 206 | this string formatString, 207 | object replacementObject, 208 | MissingKeyBehaviour missingKeyBehaviour = MissingKeyBehaviour.ThrowException, 209 | object fallbackReplacementValue = null, 210 | char openBraceChar = '{', 211 | char closeBraceChar = '}') 212 | { 213 | return FormatWithMethods.FormattableWith( 214 | formatString, 215 | replacementObject, 216 | missingKeyBehaviour, 217 | fallbackReplacementValue, 218 | openBraceChar, 219 | closeBraceChar); 220 | } 221 | 222 | /// 223 | /// Produces a representing the input format string. 224 | /// 225 | /// The format string, containing keys like {foo} 226 | /// An with keys and values to inject into the string 227 | /// The resultant 228 | public static FormattableString FormattableWith(this string formatString, IDictionary replacements) 229 | { 230 | // wrap the IDictionary in a wrapper Dictionary class that casts the values to objects as needed 231 | return FormatWithMethods.FormattableWith(formatString, replacements); 232 | } 233 | 234 | /// 235 | /// Produces a representing the input format string. 236 | /// 237 | /// The format string, containing keys like {foo} 238 | /// An with keys and values to inject into the string 239 | /// The behaviour to use when the format string contains a parameter that is not present in the lookup dictionary 240 | /// When the is specified, this string is used as a fallback replacement value when the parameter is present in the lookup dictionary. 241 | /// The character used to begin parameters 242 | /// The character used to end parameters 243 | /// The resultant 244 | public static FormattableString FormattableWith( 245 | this string formatString, 246 | IDictionary replacements, 247 | MissingKeyBehaviour missingKeyBehaviour = MissingKeyBehaviour.ThrowException, 248 | string fallbackReplacementValue = null, 249 | char openBraceChar = '{', 250 | char closeBraceChar = '}') 251 | { 252 | // wrap the IDictionary in a wrapper Dictionary class that casts the values to objects as needed 253 | return FormatWithMethods.FormattableWith( 254 | formatString, 255 | replacements, 256 | missingKeyBehaviour, 257 | fallbackReplacementValue, 258 | openBraceChar, 259 | closeBraceChar); 260 | } 261 | 262 | /// 263 | /// Produces a representing the input format string. 264 | /// 265 | /// The format string, containing keys like {foo} 266 | /// An with keys and values to inject into the string 267 | /// The resultant 268 | public static FormattableString FormattableWith(this string formatString, IDictionary replacements) 269 | { 270 | return FormatWithMethods.FormattableWith(formatString, replacements); 271 | } 272 | 273 | /// 274 | /// Produces a representing the input format string. 275 | /// 276 | /// The format string, containing keys like {foo} 277 | /// An with keys and values to inject into the string 278 | /// The behaviour to use when the format string contains a parameter that is not present in the lookup dictionary 279 | /// When the is specified, this string is used as a fallback replacement value when the parameter is present in the lookup dictionary. 280 | /// The character used to begin parameters 281 | /// The character used to end parameters 282 | /// The resultant 283 | public static FormattableString FormattableWith( 284 | this string formatString, 285 | IDictionary replacements, 286 | MissingKeyBehaviour missingKeyBehaviour = MissingKeyBehaviour.ThrowException, 287 | object fallbackReplacementValue = null, 288 | char openBraceChar = '{', 289 | char closeBraceChar = '}') 290 | { 291 | return FormatWithMethods.FormattableWith( 292 | formatString, 293 | replacements, 294 | missingKeyBehaviour, 295 | fallbackReplacementValue, 296 | openBraceChar, 297 | closeBraceChar); 298 | } 299 | 300 | /// 301 | /// Produces a representing the input format string. 302 | /// 303 | /// The format string, containing keys like {foo} 304 | /// A handler function that transforms each parameter into a 305 | /// The resultant 306 | public static FormattableString FormattableWith( 307 | this string formatString, 308 | Func handler) 309 | { 310 | return FormatWithMethods.FormattableWith(formatString, handler); 311 | } 312 | 313 | /// 314 | /// Produces a representing the input format string. 315 | /// 316 | /// The format string, containing keys like {foo} 317 | /// A handler function that transforms each parameter into a 318 | /// The behaviour to use when the format string contains a parameter that cannot be replaced by the handler 319 | /// When the is specified, this object is used as a fallback replacement value. 320 | /// The character used to begin parameters 321 | /// The character used to end parameters 322 | /// The resultant 323 | public static FormattableString FormattableWith( 324 | this string formatString, 325 | Func handler, 326 | MissingKeyBehaviour missingKeyBehaviour = MissingKeyBehaviour.ThrowException, 327 | object fallbackReplacementValue = null, 328 | char openBraceChar = '{', 329 | char closeBraceChar = '}') 330 | { 331 | return FormatWithMethods.FormattableWith( 332 | formatString, 333 | handler, 334 | missingKeyBehaviour, 335 | fallbackReplacementValue, 336 | openBraceChar, 337 | closeBraceChar); 338 | } 339 | 340 | #endregion 341 | 342 | /// 343 | /// Gets an that will return all format parameters used within the format string. 344 | /// 345 | /// The format string to be parsed 346 | /// The character used to begin parameters 347 | /// The character used to end parameters 348 | /// 349 | public static IEnumerable GetFormatParameters( 350 | this string formatString, 351 | char openBraceChar = '{', 352 | char closeBraceChar = '}') 353 | { 354 | return FormatWithMethods.GetFormatParameters(formatString, openBraceChar, closeBraceChar); 355 | } 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /FormatWithTests/FormatProvider/UpperCaseFormatProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FormatWithTests.FormatProvider 4 | { 5 | internal class UpperCaseFormatProvider : IFormatProvider 6 | { 7 | private readonly UpperCaseFormatter _formatter; 8 | 9 | public UpperCaseFormatProvider() 10 | { 11 | _formatter = new UpperCaseFormatter(); 12 | } 13 | 14 | public object GetFormat(Type formatType) 15 | { 16 | if (formatType == typeof(ICustomFormatter)) 17 | return _formatter; 18 | 19 | return null; 20 | } 21 | 22 | class UpperCaseFormatter : ICustomFormatter 23 | { 24 | public string Format(string format, object arg, IFormatProvider formatProvider) 25 | { 26 | if(arg == null) return string.Empty; 27 | 28 | if (format == "upper" && arg is string str) 29 | { 30 | return str.ToUpper(); 31 | } 32 | 33 | return arg.ToString(); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /FormatWithTests/FormatWithTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using FormatWith; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using static FormatWithTests.TestStrings; 7 | 8 | namespace FormatWithTests 9 | { 10 | public class FormatWithTests 11 | { 12 | [Fact] 13 | public void TestEmpty() 14 | { 15 | string replacement = TestFormatEmpty.FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }); 16 | Assert.Equal(TestFormatEmpty, replacement); 17 | } 18 | 19 | [Fact] 20 | public void TestNoParams() 21 | { 22 | string replacement = TestFormatNoParams.FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }); 23 | Assert.Equal(TestFormatNoParams, replacement); 24 | } 25 | 26 | [Fact] 27 | public void TestReplacement3() 28 | { 29 | string replacement = TestFormat4.FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }); 30 | Assert.Equal(TestFormat4Solution, replacement); 31 | } 32 | 33 | [Fact] 34 | public void TestParameterFormat() 35 | { 36 | string replacement = TestFormat7.FormatWith(new { Today = TestFormat7Date }); 37 | Assert.Equal(TestFormat7Solution, replacement); 38 | } 39 | 40 | [Fact] 41 | public void TestNestedPropertiesReplacements() 42 | { 43 | string replacement = TestFormat5.FormatWith(new { Foo = new { Replacement1 = Replacement1 } }); 44 | Assert.Equal(TestFormat5Solution, replacement); 45 | } 46 | 47 | [Fact] 48 | public void TestUnexpectedOpenBracketError() 49 | { 50 | try 51 | { 52 | string replacement = "abc{Replacement1}{ {Replacement2}".FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }); 53 | } 54 | catch (FormatException e) 55 | { 56 | Assert.Equal("Unexpected opening brace inside a parameter at position 19", e.Message); 57 | return; 58 | } 59 | 60 | Assert.True(false); 61 | } 62 | 63 | [Fact] 64 | public void TestUnexpectedCloseBracketError() 65 | { 66 | try 67 | { 68 | string replacement = "abc{Replacement1}{{Replacement2}".FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }); 69 | } 70 | catch (FormatException e) 71 | { 72 | Assert.Equal("Unexpected closing brace at position 31", e.Message); 73 | return; 74 | } 75 | Assert.True(false); 76 | } 77 | 78 | [Fact] 79 | public void TestUnexpectedEndOfFormatString() 80 | { 81 | try 82 | { 83 | string replacement = "abc{Replacement1}{Replacement2".FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }); 84 | } 85 | catch (FormatException e) 86 | { 87 | Assert.Equal("The format string ended before the parameter was closed. Position 30", e.Message); 88 | return; 89 | } 90 | Assert.True(false); 91 | } 92 | 93 | [Fact] 94 | public void TestKeyNotFoundError() 95 | { 96 | try 97 | { 98 | string replacement = "abc{Replacement1}{DoesntExist}".FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }); 99 | } 100 | catch (KeyNotFoundException e) 101 | { 102 | Assert.Equal("The parameter \"DoesntExist\" was not present in the lookup dictionary", e.Message); 103 | return; 104 | } 105 | Assert.True(false); 106 | } 107 | 108 | [Fact] 109 | public void TestDefaultReplacementParameter() 110 | { 111 | string replacement = "abc{Replacement1}{DoesntExist}".FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }, MissingKeyBehaviour.ReplaceWithFallback, "FallbackValue"); 112 | Assert.Equal("abcReplacement1FallbackValue", replacement); 113 | } 114 | 115 | [Fact] 116 | public void TestIgnoreMissingParameter() 117 | { 118 | string replacement = "abc{Replacement1}{DoesntExist}".FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }, MissingKeyBehaviour.Ignore); 119 | Assert.Equal("abcReplacement1{DoesntExist}", replacement); 120 | } 121 | 122 | [Fact] 123 | public void TestCustomBraces() 124 | { 125 | string replacement = "abc".FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }, MissingKeyBehaviour.Ignore, null, '<', '>'); 126 | Assert.Equal("abcReplacement1", replacement); 127 | } 128 | 129 | [Fact] 130 | public void TestAsymmetricCustomBraces() 131 | { 132 | string replacement = "abc{Replacement1>{DoesntExist>".FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }, MissingKeyBehaviour.Ignore, null, '{', '>'); 133 | Assert.Equal("abcReplacement1{DoesntExist>", replacement); 134 | } 135 | 136 | [Fact] 137 | public void TestCustomHandler1() 138 | { 139 | string replacement = "Hey, {make this uppercase!} Thanks.".FormatWith( 140 | (parameter, format) => new ReplacementResult(true, parameter.ToUpper()) 141 | ); 142 | 143 | Assert.Equal("Hey, MAKE THIS UPPERCASE! Thanks.", replacement); 144 | } 145 | 146 | [Fact] 147 | public void TestCustomHandler2() 148 | { 149 | string replacement = ", , .".FormatWith( 150 | (parameter, format) => 151 | { 152 | switch (format) 153 | { 154 | case "uppercase": 155 | return new ReplacementResult(true, parameter.ToUpper()); 156 | case "lowercase": 157 | return new ReplacementResult(true, parameter.ToLower()); 158 | case "reverse": 159 | return new ReplacementResult(true, new string(parameter.Reverse().ToArray())); 160 | default: 161 | return new ReplacementResult(false, parameter); 162 | } 163 | }, 164 | MissingKeyBehaviour.ReplaceWithFallback, 165 | "Fallback", 166 | '<', 167 | '>' 168 | ); 169 | 170 | Assert.Equal("321FEDcba, ABCDEF123, abcdef123.", replacement); 171 | } 172 | 173 | [Fact] 174 | public void SpeedTest() 175 | { 176 | Dictionary replacementDictionary = new Dictionary() 177 | { 178 | ["Replacement1"] = Replacement1, 179 | ["Replacement2"] = Replacement2 180 | }; 181 | 182 | for (int i = 0; i < 1000000; i++) 183 | { 184 | string replacement = TestFormat3.FormatWith(replacementDictionary); 185 | } 186 | } 187 | 188 | [Fact] 189 | public void SpeedTestBigger() 190 | { 191 | Dictionary replacementDictionary = new Dictionary() 192 | { 193 | ["Replacement1"] = Replacement1, 194 | ["Replacement2"] = Replacement2 195 | }; 196 | 197 | for (int i = 0; i < 1000000; i++) 198 | { 199 | string replacement = TestFormat4.FormatWith(replacementDictionary); 200 | } 201 | } 202 | 203 | [Fact] 204 | public void SpeedTestBiggerAnonymous() 205 | { 206 | for (int i = 0; i < 1000000; i++) 207 | { 208 | string replacement = TestFormat4.FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }); 209 | } 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /FormatWithTests/FormatWithTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /FormatWithTests/FormattableWithTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Xunit; 4 | using FormatWith; 5 | using FormatWithTests.FormatProvider; 6 | using static FormatWithTests.TestStrings; 7 | 8 | namespace FormatWithTests 9 | { 10 | public class FormattableWithTests 11 | { 12 | [Fact] 13 | public void TestEmpty() 14 | { 15 | FormattableString formattableString = TestFormatEmpty.FormattableWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }); 16 | Assert.Equal(TestFormatEmpty, formattableString.Format); 17 | Assert.Equal(0, formattableString.ArgumentCount); 18 | Assert.Equal(TestFormatEmpty, formattableString.ToString()); 19 | } 20 | 21 | [Fact] 22 | public void TestNoParams() 23 | { 24 | FormattableString formattableString = TestFormatNoParams.FormattableWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }); 25 | Assert.Equal(TestFormatNoParams, formattableString.Format); 26 | Assert.Equal(0, formattableString.ArgumentCount); 27 | Assert.Equal(TestFormatNoParams, formattableString.ToString()); 28 | } 29 | 30 | [Fact] 31 | public void TestReplacement3() 32 | { 33 | FormattableString formattableString = TestFormat3.FormattableWith(new { Replacement1 = Replacement1 }); 34 | Assert.Equal(TestFormat3Composite, formattableString.Format); 35 | Assert.Equal(1, formattableString.ArgumentCount); 36 | Assert.Equal(Replacement1, formattableString.GetArgument(0)); 37 | Assert.Equal(TestFormat3Solution, formattableString.ToString()); 38 | } 39 | 40 | [Fact] 41 | public void TestReplacement4() 42 | { 43 | FormattableString formattableString = TestFormat4.FormattableWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }); 44 | Assert.Equal(TestFormat4Composite, formattableString.Format); 45 | Assert.Equal(2, formattableString.ArgumentCount); 46 | Assert.Equal(Replacement1, formattableString.GetArgument(0)); 47 | Assert.Equal(Replacement2, formattableString.GetArgument(1)); 48 | Assert.Equal(TestFormat4Solution, formattableString.ToString()); 49 | } 50 | 51 | [Fact] 52 | public void TestNestedProperties() 53 | { 54 | FormattableString formattableString = TestFormat5.FormattableWith(new { Foo = new { Replacement1 = Replacement1 } }); 55 | Assert.Equal(TestFormat5Composite, formattableString.Format); 56 | Assert.Equal(1, formattableString.ArgumentCount); 57 | Assert.Equal(Replacement1, formattableString.GetArgument(0)); 58 | Assert.Equal(TestFormat5Solution, formattableString.ToString()); 59 | } 60 | 61 | [Fact] 62 | public void TestFormatString() 63 | { 64 | FormattableString formattableString = TestFormat6.FormattableWith(new { Replacement1 = Replacement1 }); 65 | Assert.Equal(TestFormat6Composite, formattableString.Format); 66 | Assert.Equal(1, formattableString.ArgumentCount); 67 | Assert.Equal(Replacement1, formattableString.GetArgument(0)); 68 | 69 | var upperCaseFormatProvider = new UpperCaseFormatProvider(); 70 | 71 | Assert.Equal(TestFormat6Solution, formattableString.ToString(upperCaseFormatProvider)); 72 | } 73 | 74 | [Fact] 75 | public void TestCustomBraces() 76 | { 77 | string format = "abc{{Replacement1}"; 78 | string formatComposite = "abc{{{{Replacement1}}{0}"; 79 | string formatSolution = $"abc{{{{Replacement1}}{Replacement2}"; 80 | FormattableString formattableString = format.FormattableWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }, MissingKeyBehaviour.ThrowException, null, '<', '>'); 81 | Assert.Equal(formatComposite, formattableString.Format); 82 | Assert.Equal(1, formattableString.ArgumentCount); 83 | Assert.Equal(Replacement2, formattableString.GetArgument(0)); 84 | Assert.Equal(formatSolution, formattableString.ToString()); 85 | } 86 | 87 | [Fact] 88 | public void TestAsymmetricCustomBracesWithIgnore() 89 | { 90 | string format = "abc{Replacement1>{DoesntExist>"; 91 | string formatComposite = "abc{0}{{DoesntExist>"; 92 | string formatSolution = $"abc{Replacement1}{{DoesntExist>"; 93 | 94 | FormattableString formattableString = format.FormattableWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }, MissingKeyBehaviour.Ignore, null, '{', '>'); 95 | Assert.Equal(formatComposite, formattableString.Format); 96 | Assert.Equal(1, formattableString.ArgumentCount); 97 | Assert.Equal(Replacement1, formattableString.GetArgument(0)); 98 | Assert.Equal(formatSolution, formattableString.ToString()); 99 | } 100 | 101 | [Fact] 102 | public void SpeedTest() 103 | { 104 | Dictionary replacementDictionary = new Dictionary() 105 | { 106 | ["Replacement1"] = Replacement1, 107 | ["Replacement2"] = Replacement2 108 | }; 109 | 110 | for (int i = 0; i < 1000000; i++) 111 | { 112 | FormattableString formattableString = TestFormat3.FormattableWith(replacementDictionary); 113 | } 114 | } 115 | 116 | [Fact] 117 | public void SpeedTestBigger() 118 | { 119 | Dictionary replacementDictionary = new Dictionary() 120 | { 121 | ["Replacement1"] = Replacement1, 122 | ["Replacement2"] = Replacement2 123 | }; 124 | 125 | for (int i = 0; i < 1000000; i++) 126 | { 127 | FormattableString formattableString = TestFormat4.FormattableWith(replacementDictionary); 128 | } 129 | } 130 | 131 | [Fact] 132 | public void SpeedTestBiggerAnonymous() 133 | { 134 | for (int i = 0; i < 1000000; i++) 135 | { 136 | FormattableString formattableString = TestFormat4.FormattableWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }); 137 | } 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /FormatWithTests/MiscTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Linq; 5 | using Xunit; 6 | using FormatWith; 7 | using static FormatWithTests.TestStrings; 8 | 9 | namespace FormatWithTests 10 | { 11 | public class MiscTests 12 | { 13 | [Fact] 14 | public void TestMethodPassing() 15 | { 16 | // test to make sure testing framework is working (!) 17 | Assert.True(true); 18 | } 19 | 20 | [Fact] 21 | public void TestGetFormatParameters() 22 | { 23 | List parameters = TestFormat4.GetFormatParameters().ToList(); 24 | Assert.Equal(parameters.Count, 2); 25 | Assert.Equal(nameof(Replacement1), parameters[0]); 26 | Assert.Equal(nameof(Replacement2), parameters[1]); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /FormatWithTests/TestStrings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace FormatWithTests 6 | { 7 | public static class TestStrings 8 | { 9 | public static readonly string Replacement1 = "Replacement1"; 10 | public static readonly string Replacement2 = "Replacement {} Two "; 11 | 12 | public static readonly string TestFormatEmpty = ""; 13 | 14 | public static readonly string TestFormatNoParams = "Test string with no parameters"; 15 | 16 | public static readonly string TestFormat3 = "test{Replacement1}"; 17 | public static readonly string TestFormat3Composite = "test{0}"; 18 | public static readonly string TestFormat3Solution = $"test{Replacement1}"; 19 | 20 | public static readonly string TestFormat4 = "abc{Replacement1}def{{escaped1}}ghi{{{Replacement2}}}jkl{{{{escaped2}}}}mno"; 21 | public static readonly string TestFormat4Composite = "abc{0}def{{escaped1}}ghi{{{1}}}jkl{{{{escaped2}}}}mno"; 22 | public static readonly string TestFormat4Solution = $"abc{Replacement1}def{{escaped1}}ghi{{{Replacement2}}}jkl{{{{escaped2}}}}mno"; 23 | 24 | public static readonly string TestFormat5 = "abc{Foo.Replacement1}"; 25 | public static readonly string TestFormat5Composite = "abc{0}"; 26 | public static readonly string TestFormat5Solution = $"abc{Replacement1}"; 27 | 28 | public static readonly string TestFormat6 = "abc{Replacement1:upper}"; 29 | public static readonly string TestFormat6Composite = "abc{0:upper}"; 30 | public static readonly string TestFormat6Solution = $"abc{Replacement1.ToUpper()}"; 31 | 32 | public static readonly string TestFormat7 = "Today is {Today:YYYYMMDD HH:mm}"; 33 | public static readonly string TestFormat7Composite = "Today is {0:YYYYMMDD HH:mm}"; 34 | public static readonly DateTime TestFormat7Date = new DateTime(2018, 10, 30, 17, 25, 0); 35 | public static readonly string TestFormat7Solution = $"Today is {TestFormat7Date:YYYYMMDD HH:mm}"; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ryan Crosby 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FormatWith 2 | 3 | [![NuGet](https://img.shields.io/nuget/v/FormatWith)](https://www.nuget.org/packages/FormatWith/) 4 | [![license](https://img.shields.io/github/license/crozone/FormatWith)](https://github.com/crozone/FormatWith/blob/master/License.txt) 5 | ![Build status](https://github.com/crozone/FormatWith/workflows/CI/badge.svg) 6 | 7 | A set of string extension methods for performing {named} {{parameterized}} string formatting, written for NetStandard 2.0. 8 | 9 | ## Quick Info 10 | 11 | This library provides named string formatting via the string extension .FormatWith(). It formats strings against a lookup dictionary, anonymous type, or handler. 12 | 13 | It is written as a Net Standard 2.0 class library, published as a NuGet package, and is fully compatible with any .NET platform that implements NetStandard 2.0. This makes it compatible with .NET Core 2.0, .NET Full Framework 4.6.1, UWP/UAP 10, and most mono/xamarin platforms. 14 | 15 | An example of what it can do: 16 | 17 | using FormatWith; 18 | ... 19 | string formatString = "Your name is {name}, and this is {{escaped}}, this {{{works}}}, and this is {{{{doubleEscaped}}}}"; 20 | 21 | // format the format string using the FormatWith() string extension. 22 | // We can parse in replacement parameters as an anonymous type 23 | string output = formatString.FormatWith(new { name = "John", works = "is good" }); 24 | 25 | // output now contains the formatted text. 26 | Console.WriteLine(output); 27 | 28 | Produces: 29 | 30 | > "Your name is John, and this is {escaped}, this {is good}, and this is {{doubleEscaped}}" 31 | 32 | It can also be fed parameters via an `IDictionary` or an `IDictionary`, rather than a type. 33 | 34 | The value of each replacement parameter is given by whatever the objects `.ToString()` method produces. This value is not cached, so you can get creative with the implementation (the object is fed directly into a StringBuilder). 35 | 36 | ## How it works 37 | 38 | A state machine parser quickly runs through the input format string, tokenizing the input into tokens of either "normal" or "parameter" text. These tokens are simply a struct with an index and length into the original format string - `SubString()` is avoided to prevent unnecessary string allocations. These are fed out of an enumerator right into a `StringBuilder`. Since `StringBuilder` is pre-allocated a small chunk of memory, and only `.Append()`ed relatively large segments of string, it produces the final output string quickly and efficiently. 39 | 40 | ## Extension methods: 41 | 42 | Three extension methods for `string` are defined in `FormatWith.StringExtensions`: `FormatWith()`, `FormattableWith()`, and `GetFormatParameters()`. 43 | 44 | ### FormatWith 45 | 46 | The first, second, and third overload of `FormatWith()` take a format string containing named parameters, along with an object, dictionary, or function for providing replacement parameters. Optionally, missing key behaviour, a fallback value, and custom brace characters can be specified. Two adjacent opening or closing brace characters in the format string are treated as escaped, and will be reduced to a single brace in the output string. 47 | 48 | Missing key behaviour is specified by the `MissingKeyBehaviour` enum, which can be `ThrowException`, `ReplaceWithFallback`, or `Ignore`. 49 | 50 | `ThrowException` throws a `KeyNotFoundException` if a replacement value for a parameter in the format string could not be found. 51 | 52 | `ReplaceWithFallback` inserts the value specified by `fallbackReplacementValue` in place of any parameters that could not be replaced. If an object-based overload is used, `fallbackReplacementValue` is an `object`, and the string representation of the object will be resolved as the value. 53 | 54 | `Ignore` ignores any parameters that did not have a corresponding key in the lookup dictionary, leaving the unmodified braced parameter in the output string. This is useful for tiered formatting. 55 | 56 | **Examples:** 57 | 58 | `string output = "abc {Replacement1} {DoesntExist}".FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }); 59 | 60 | output: Throws a `KeyNotFoundException` with the message "The parameter \"DoesntExist\" was not present in the lookup dictionary". 61 | 62 | `string output = "abc {Replacement1} {DoesntExist}".FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }, MissingKeyBehaviour.ReplaceWithFallback, "FallbackValue");` 63 | 64 | output: "abc Replacement1 FallbackValue" 65 | 66 | `string replacement = "abc {Replacement1} {DoesntExist}".FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }, MissingKeyBehaviour.Ignore); 67 | 68 | output: "abc Replacement1 {DoesntExist}" 69 | 70 | **Using custom brace characters:** 71 | 72 | Custom brace characters can be specified for both opening and closing parameters, if required. 73 | 74 | `string replacement = "abc ".FormatWith(new { Replacement1 = Replacement1, Replacement2 = Replacement2 }, MissingKeyBehaviour.Ignore, null,'<','>');` 75 | 76 | output: "abc Replacement1 " 77 | 78 | ### FormattableWith 79 | 80 | The first, second, and third overload of FormattableWith() function much the same way that the FormatWith() overloads do. However, FormattableWith returns a `FormattableString` instead of a `string`. This allows parameters and composite format string to be inspected, and allows a custom formatter to be used if desired. 81 | 82 | ### Handler overloads 83 | 84 | A custom handler can be passed to both FormatWith() and FormattableWith(). The handler is passed the value of each parameter key and format (if applicable). It is responsible for providing a `ReplacementResult` in response. The `ReplacementResult` contains the `Value` which will be substituted, as well as a boolean `Success` parameter indicating whether the replacement was successful. If `Success` is false, the `MissingKeyBehaviour` is followed, as per the other overloads of FormatWith. 85 | 86 | This can allow for some neat tricks, and even complex behaviours. 87 | 88 | Example: 89 | 90 | "{abcDEF123:reverse}, {abcDEF123:uppercase}, {abcDEF123:lowercase}.".FormatWith( 91 | (parameter, format) => 92 | { 93 | switch (format) 94 | { 95 | case "uppercase": 96 | return new ReplacementResult(true, parameter.ToUpper()); 97 | case "lowercase": 98 | return new ReplacementResult(true, parameter.ToLower()); 99 | case "reverse": 100 | return new ReplacementResult(true, new string(parameter.Reverse().ToArray())); 101 | default: 102 | return new ReplacementResult(false, parameter); 103 | } 104 | }); 105 | 106 | Produces: 107 | 108 | "321FEDcba, ABCDEF123, abcdef123." 109 | 110 | ### GetFormatParameters 111 | 112 | `GetFormatParameters()` can be used to get a list of parameter names out of a format string, which can be used for inspecting a format string before performing other actions on it. 113 | 114 | **Example:** 115 | 116 | `IEnumerable parameters = "{parameter1} {parameter2} {{not a parameter}}".GetFormatParameters();` 117 | 118 | output: The enumerable will return "parameter1","parameter2" during iteration. 119 | 120 | ## Tests: 121 | 122 | A testing project is included that has coverage of most scenarios involving the three extension methods. The testing framework in use is xUnit. 123 | 124 | ## Performance: 125 | 126 | The SpeedTest test function performs 1,000,000 string formats, with a format string containing 1 parameter. On a low end 1.3Ghz mobile i7, this completes in around 700ms, giving ~1.4 million replacements per second. 127 | 128 | The SpeedTestBigger test performs a more complex replacement on a longer string containing 2 parameters and several escaped brackets, again 1,000,000 times. On the same hardware, this test completed in around 1 seconds. 129 | 130 | The SpeedTestBiggerAnonymous test is the same as SpeedTestBigger, but uses the anonymous function overload of FormatWith. It completes in just under 2 seconds. Using the anonymous overload of FormatWith is slightly slower due to reflection overhead, although this is minimised by caching. 131 | 132 | So as a rough performance guide, FormatWith will usually manage about 1 million parameter replacements per second on low end hardware. 133 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crozone/FormatWith/35671b04415185341517915345b0783bbf162073/icon.png --------------------------------------------------------------------------------