Introduction
There are a few frequently convenient extended formatting actions that are just not possible with the standard
implementation of String.Format()
.
For example, centering your value in the field, or getting the field width from a variable in the argument list.
Background
The question of C# Indirect String Formatting came to my attention when I stumbled on these questions:
user72491 wanted the missing String.PadCenter(width)
, which Joel provided for him.
Ken was asking for a C# way to do the typical C formatting thing of using an argument to specify
the width field for another argument's formatting.
int width = 5;
int value = 123;
printf( "This is formatted: |%*s| %d wide.", width, value, width);
Ken wanted to avoid the cumbersome style of gluing together the format string:
int width = 5;
int value = 123;
Console.WriteLine(
"This is formatted: |{0, "
+ width.ToString()
+ "}| {1} wide.",
value, width);
Not only is this prone to errors, but would have to be done in a custom way each and every time the functionality was needed.
Various answers were offered, and links were provided to other articles with similar questions and answers.
I decided to encapsulating all the answers and suggestions into a single method, with ideas of my own, and to make it generically useful.
And generically useful code is worth sharing. ;-D
But my answer got too long for a simple answer post at
stackoverflow, thus this richer article and a link over there.
Heath Stewart's article Custom String Formatting in .NET
gives a good overview of the many things one can do with the standard formatting field placeholders.
Or you can slog through the MSDN article Composite Formatting for all the gory details.
FormatEx
is a pre-processor that converts an extended formatting field into a standard one that accomplishes the desired goal.
It then calls String.Format()
and returns the final result.
Using the code
Basically, any place you would call String.Format()
, you could instead call FormatEx()
. All standard formatting field descriptions still work.
Standard formatting place holders
The standard formatting placeholder look like this (see the MSDN article see Composite Formatting for all the gory details):
{index,alignment:formatString}
Where:
| index | | is the zero based index into the arguments for the value to format. |
| , alignment | | is an optional positive integer for right alignment, or a negative integer for left alignment. |
| : formatString | | is an optional string describing the style for the format. |
Extended formatting place holders
The extension Ken would have like to have seen might look like these:
{index1,{index2}}
{index1,{index2}:formatString}
Since I am, however, adding three extensions in this article, it might also look like any of these:
{index1:{index3}}
{index1,{index2}:{index3}}
{index1,alignment:{index3}}
{index1,c{index2}:formatString}
{index1,c alignment:{index3}}
{index1,c alignment:formatString}
{index1,c{index2}:{index3}}
Where:
| index1 | | is the zero based index into the arguments for the value to format. |
| c | | is an optional flag to indicate center alignment |
| index2 | | is the zero based index into the arguments for alignment width value. |
| index3 | | is the zero based index into the arguments for string describing the style for the format. |
| alignment | | is an optional positive integer for right alignment, or a negative integer for left alignment. |
| formatString | | is an optional string describing the style for the format. |
Center Alignment
Centering the value in the width field has a couple of tricky spots:
- If you are also specifying a
formatString
.
The formatString
has to be applied to the value first, then the formatted result centered in the width.
- If the value to be centered needs an odd number of pad spaces to fill the width.
Since alignment normally does right for positive and left for negative values,
I figured this was the clue needed to adjust the center padding.
- If the same argument value is to be formatted more than one way in the same call.
The original value must be used for each special formatting.
For example:
Console.WriteLine("|{0,{1}}| and |{0,{2}}|", 123, 5, -5);
Indirect Alignment
Using the nested {index2} to help me fetch the final alignment specifier, just takes some parsing and array indexing.
The regular expression let me know whether I have an indirect index to an alignment width,
or whether I have a standard alignment width to work with.
Indirect Formatting code string
Likewise, the nested {index3} lets me retrieve the passed in formatString
to use for that field.
The regular expression let me know whether I have an indirect index to a formatString
,
or whether I have a standard formatString
to work with.
FormatEx, The Structure
I use a regular expression to find all the field placeholders, whether they contain my extensions or not.
If a field place holder has any of the extensions, then two things might happen:
- the field place holder is replaced with a simpler place holder, without indirection.
- the argument in the
params Object[] varArgs
is formatted as requested
and the new value is appended to the end of the arguments list.
Then, finally, the simplified format string and the adjusted arguments array are passed
to String.Format
for final formatting.
Because String.Format
has a version with a CultureInfo
argument,
I define one of those for FormatEx as well.
I do not bother to define all the other argument signatures like String.Format does, as it seems to me that
params Object[] varArgs
catches everything.
FormatEx, Example Calls
Here are a few example calls to show you what can be done.
Console.WriteLine("Value = |{0}|\nExpecting = |{1}|\n",
FormatEx("|{0,{1}}|", "test", 10), "| test|");
Console.WriteLine("Value = |{0}|\nExpecting = |{1}|\n",
FormatEx("|{0,{1}}|", "test", -10),
"|test |");
Console.WriteLine("Value = |{0}|\nExpecting = |{1}|\n",
FormatEx("|{0,c{1}}|", "test", 10),
"| test |");
Console.WriteLine("Value = |{0}|\nExpecting = |{1}|\n",
FormatEx("|{0,{1}:{2}}|", 123.456, -10, "F1"),
"|123.5 |");
Console.WriteLine("Value = |{0}|\nExpecting = |{1}|\n",
FormatEx("|{0,c{1}:{2}}|", 123.456, -10, "F1"),
"| 123.5 |");
Console.WriteLine("Value = |{0}|\nExpecting = |{1}|\n",
FormatEx("|{0,c{1}:{2}}|", 123.456, -10, "F1"),
"| 123.5 |");
Console.WriteLine("Value = |{0}|\nExpecting = |{1}|\n",
FormatEx("|{0,c{1}:{2}}|", 123.456, 10, "F1"),
"| 123.5 |");
Console.WriteLine("Value = |{0}|\nExpecting = |{1}|\n",
FormatEx("|{0,c-{1}:{2}}|", 123.456, -10, "F1"),
"| 123.5 |");
Console.WriteLine("Value = |{0}|\nExpecting = |{1}|\n",
FormatEx("|{0,{1}:{2}}|", 123.456, 10, "F1"),
"| 123.5|");
Console.WriteLine("Value = |{0}|\nExpecting = |{1}|\n",
FormatEx("|{0,{1}:{2}}|", 3.1415926535, 10, "F2"),
"| 3.14|");
Console.WriteLine("Value = |{0}|\nExpecting = |{1}|\n",
FormatEx("|{0,c{1}:{2}}|", 3.1415926535, 10, "F2"),
"| 3.14 |");
The output should look like this:
Value = || test||
Expecting = || test||
Value = ||test ||
Expecting = ||test ||
Value = || test ||
Expecting = || test ||
Value = ||123.5 ||
Expecting = ||123.5 ||
Value = || 123.5 ||
Expecting = || 123.5 ||
Value = || 123.5 ||
Expecting = || 123.5 ||
Value = || 123.5 ||
Expecting = || 123.5 ||
Value = || 123.5 ||
Expecting = || 123.5 ||
Value = || 123.5||
Expecting = || 123.5||
Value = || 3.14||
Expecting = || 3.14||
Value = || 3.14 ||
Expecting = || 3.14 ||
FormatEx, The Code
namespace CTC
{
#region class Utilities
public static partial class Utilities
{
#region FormatEx
public static String FormatEx(String format, params Object[] varArgs)
{
if (String.IsNullOrEmpty(format))
{
throw new ArgumentNullException(
"format",
"The 'format' string may not be null or empty.");
}
return FormatEx(CultureInfo.CurrentUICulture, format, varArgs);
}
public static String FormatEx(CultureInfo uiCulture,
String format,
params Object[] varArgs)
{
if (String.IsNullOrEmpty(format))
{
throw new ArgumentNullException(
"format",
"The 'format' string may not be null or empty.");
}
if (null == uiCulture)
{
uiCulture = CultureInfo.CurrentUICulture;
}
Regex reTotal = new Regex(@"{(\d+)(,(c)?(-)?(({(\d+)})|(\d+)))?(:(({(\d+)})|([^}]+)))?}");
MatchCollection matches;
matches = reTotal.Matches(format);
if ((null != matches) && (0 < matches.Count))
{
if ((null == varArgs) || (0 == varArgs.Length))
{
throw new ArgumentNullException(
"varArgs",
String.Format(
CultureInfo.InvariantCulture,
"You specified {0} formatting placeholder{1}"
+ " but varArgs is null or empty.",
matches.Count,
1 == matches.Count ? "" : "s"));
}
List<Object> extArgs = new List<object>(varArgs);
for (int m = matches.Count; --m >= 0; )
{
String fieldFormat = matches[m].Groups[0].Value;
int argV = int.Parse(matches[m].Groups[1].Value);
if ((argV < 0) || (varArgs.Length <= argV))
{
throw new IndexOutOfRangeException(
String.Format(
CultureInfo.InvariantCulture,
"You specified formatting for argument [{0}]"
+ " but the legal index range is"
+ " [0 .. {1}] inclusive.",
argV,
varArgs.Length - 1));
}
if (String.IsNullOrEmpty(matches[m].Groups[3].Value)
&& String.IsNullOrEmpty(matches[m].Groups[6].Value)
&& String.IsNullOrEmpty(matches[m].Groups[11].Value))
{
continue;
}
String formatPart = matches[m].Groups[9].Value;
if (!String.IsNullOrEmpty(formatPart))
{
if (!String.IsNullOrEmpty(matches[m].Groups[11].Value))
{
int argF = int.Parse(matches[m].Groups[12].Value);
if ((argF < 0) || (varArgs.Length <= argF))
{
throw new IndexOutOfRangeException(
String.Format(
CultureInfo.InvariantCulture,
"You specified Indirect"
+ " formatString"
+ " for argument [{0}]"
+ " from [{1}] but the legal"
+ " index range is"
+ " [0 .. {2}] inclusive.",
argV,
argF,
varArgs.Length - 1));
}
formatPart = String.Format(uiCulture,
":{0}",
varArgs[argF]);
}
}
bool centered = !String.IsNullOrEmpty(matches[m].Groups[3].Value);
if (centered
|| !String.IsNullOrEmpty(matches[m].Groups[6].Value))
{
int width;
if (!String.IsNullOrEmpty(matches[m].Groups[6].Value))
{
int argW = int.Parse(matches[m].Groups[7].Value);
if ((argW < 0) || (varArgs.Length <= argW))
{
throw new IndexOutOfRangeException(
String.Format(
CultureInfo.InvariantCulture,
"You specified Indirect Alignment"
+ " for argument [{0}]"
+ " from argument {1} but the"
+ " legal index range is"
+ " [0 .. {2}] inclusive.",
argV,
argW,
varArgs.Length - 1));
}
String indirectWidth = String.Format("{0}", varArgs[argW]);
if (indirectWidth.StartsWith("c", StringComparison.OrdinalIgnoreCase))
{
centered = true;
indirectWidth = indirectWidth.Substring(1);
}
width = int.Parse(indirectWidth);
}
else
{
width = int.Parse(matches[m].Groups[8].Value);
}
if (!String.IsNullOrEmpty(matches[m].Groups[4].Value))
{
width = -width;
}
if (centered)
{
String argValue = String.Format(uiCulture,
"{0"
+ formatPart
+ "}",
varArgs[argV]);
if (width < 0)
{
width = -width;
if (argValue.Length < width)
{
int padding = argValue.Length
+ ((width - argValue.Length) / 2);
argValue = argValue.PadLeft(padding).PadRight(width);
}
}
else
{
if (argValue.Length < width)
{
int padding = argValue.Length
+ (((width - argValue.Length) + 1) / 2);
argValue = argValue.PadLeft(padding).PadRight(width);
}
}
argV = extArgs.Count;
extArgs.Add(argValue);
fieldFormat = "{"
+ argV.ToString()
+ ","
+ width.ToString()
+ "}";
format = format.Substring(0, matches[m].Groups[0].Index)
+ fieldFormat
+ format.Substring(matches[m].Groups[0].Index
+ matches[m].Groups[0].Length);
continue;
}
else
{
fieldFormat = "{"
+ matches[m].Groups[1].Value
+ ","
+ width.ToString()
+ formatPart + "}";
format = format.Substring(0, matches[m].Groups[0].Index)
+ fieldFormat
+ format.Substring(matches[m].Groups[0].Index
+ matches[m].Groups[0].Length);
continue;
}
}
fieldFormat = "{"
+ matches[m].Groups[1].Value
+ matches[m].Groups[2].Value
+ formatPart
+ "}";
format = format.Substring(0, matches[m].Groups[0].Index)
+ fieldFormat
+ format.Substring(matches[m].Groups[0].Index
+ matches[m].Groups[0].Length);
}
varArgs = extArgs.ToArray();
}
return String.Format(uiCulture, format, varArgs);
}
#endregion FormatEx
}
#endregion class Utilities
}
Points of Interest
I was reminded by this project just how powerful Regular Expressions can be.
My original working code had three different expressions, one for each extension style.
Then, during testing, I found that processing them each separately did not lend itself
to using two or three extensions at the same time.
The new combined Regular Expression matches all legal standard or extended field formatting placeholders.
And I have all the parsed out information I need to put it back together in the best way.
Here it is again, in all its glory.
Regex reTotal = new Regex(@"{(\d+)(,(c)?(-)?(({(\d+)})|(\d+)))?(:(({(\d+)})|([^}]+)))?}");
Conclusion
Well, there it is. A method that works like String.Format
, but with some nested extensions.
It may be a memory-hog, since it copies the argument array when there is formatting to be done. YMMV.
I originally replaced the values in the varArgs array. But that failed miserably when the caller uses the same value in more then one placeholder with different formatting.
For example:
Console.WriteLine("Value = |{0}|\nExpecting = |{1}|\n",
Utilities.FormatEx("|{0,{1}:{2}}|{0,{3}:{4}}|{0,{5}:{6}}|",
3.1415926535,
10, "F2",
-10, "F3",
"c10", "F2"),
"| 3.14|3.142 | 3.14 |");
To produce:
Value = || 3.14|3.142 | 3.14 ||
Expecting = || 3.14|3.142 | 3.14 ||
I hope you find this article and code useful, or at least informative.
History
FormatEx Code Revision History |
Date | Version | Notes |
2012.06.13 | 1.0.0.1 | Tweaked in-line documentation (in the attached ZIP file). |
2012.06.13 | 1.0.0.0 | Initial creation of
FormatEx code. |
|
---|
FormatEx Article Revision History |
Date | Version | Notes |
2012.06.13 | 1.0.0.1 | Cosmetic tweaks to adjust fro a too-smart-for-my-own-good submission wizard. |
2012.06.13 | 1.0.0.0 | Initial creation of article and the demo console application. |
Disclaimer
I work as a Software Engineer for American Dynamics (a subsidiary of Tyco International).
This code was not part of any project undertaken for American Dynamics.
I also sometimes work as a self-employed consultant.
This code was not part of any project undertaken as part of any such contract.