Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A C# way for indirect width and style formatting.

0.00/5 (No votes)
14 Jun 2012 1  
FormatEx is a method that allows structuring formatting placeholders indirectly from arguments.

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.

// prints 123 as "  123"
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:

// prints 123 as "  123"
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);
    //    expect: |  123| and |123  |
    

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:

  1. the field place holder is replaced with a simpler place holder, without indirection.
  2. 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),      // get the formatted result
    "|      test|");                        // the expected return value

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

// Just a namespace to flag this class as a product of Chisholm Trail Consulting.
namespace CTC
{
    #region class Utilities
    // Just a class name to hold utility functions.
    public static partial class Utilities
    {
        #region FormatEx
        // Inline XML documentation deleted to save space.
        // See the attached source file for the full inline documentation.
        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);
        }

        // Inline XML documentation deleted to save space.
        // See the attached source file for the full inline documentation.
        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;
            }

            // reTotal loosely matches any legal formatting placeholder:
            //
            //  [0]             ==  "{...}"     ==  the entire placeholder
            //  [1]                             ==  index into varArgs for the value
            //  [2]             ==  ",..."      ==  optional alignment specifier
            //      [3]         ==  "c"         ==  optional center modifier
            //      [4]         ==  "-"         ==  optional left modifier
            //      [5]         ==  "..."       ==  alignment width option
            //          [6]     ==  "{N}"       ==  bracketed index to
            //                                      indirect alignment width
            //          [7]     ==  "N"         ==  index into varArgs
            //                                      for the alignment width
            //          [8]     ==  "N"         ==  actual value of
            //                                      alignment width
            //                                      (not bracketed, so not indirect)
            //  [9]             ==  ":..."      ==  optional formatting
            //      [10]        ==  "..."       ==  alignnemt width option
            //          [11]    ==  "{N}"       ==  bracketed index to
            //                                      the indirect formatting argument
            //          [12]    ==  "N"         ==  index into varArgs
            //                                      for the indirect formatting
            //                                      code string
            //          [13]    ==  "..."       ==  actual value of
            //                                      formatting
            //                                      (not bracketed, so not indirect)
            //
            Regex reTotal = new Regex(@"{(\d+)(,(c)?(-)?(({(\d+)})|(\d+)))?(:(({(\d+)})|([^}]+)))?}");
            MatchCollection matches;

            // retrieve _all_ matches of this RE on the format string.
            //
            matches = reTotal.Matches(format);

            // Only have work to do if there are field place holders
            //
            if ((null != matches) && (0 < matches.Count))
            {
                // place holders specified, but no values provided?
                //
                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"));
                }

                // Clone the arguments,
                //  as we need to extend the array for the center formatted values.
                //
                List<Object>  extArgs = new List<object>(varArgs);

                // walk matches in reverse order so indexes
                //  for early ones don't change before I use them.
                //
                for (int m = matches.Count; --m >= 0; )
                {
                    // original field format, with possible extensions
                    //
                    String fieldFormat = matches[m].Groups[0].Value;

                    // get the index to the value to be formatted
                    //
                    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));
                    }

                    // Nothing unusual unless [3], [6] or [11]
                    //
                    if (String.IsNullOrEmpty(matches[m].Groups[3].Value)
                     && String.IsNullOrEmpty(matches[m].Groups[6].Value)
                     && String.IsNullOrEmpty(matches[m].Groups[11].Value))
                    {
                        // Nope!  No extensions asked for.
                        //  we can leave this format placeholder
                        //  and the varArgs list alone.
                        //
                        continue;
                    }

                    // if they asked for an indirect formatting code,
                    //  then we need to calculate what it is.
                    // We will need if they are centering,
                    //  and we will want to de-indirect it if they are not.
                    //
                    String formatPart = matches[m].Groups[9].Value;
                    if (!String.IsNullOrEmpty(formatPart))
                    {
                        if (!String.IsNullOrEmpty(matches[m].Groups[11].Value))
                        {
                            // get the index to the formatString to be used
                            //
                            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]);
                        }
                    }

                    // if we are aligning special
                    //
                    bool centered = !String.IsNullOrEmpty(matches[m].Groups[3].Value);
                    if (centered
                     || !String.IsNullOrEmpty(matches[m].Groups[6].Value))
                    {
                        // whether direct or indirect, get the non-indirect width
                        //
                        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))
                            {
                                // indirect centering
                                //
                                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 centering
                        //
                        if (centered)
                        {
                            // format the final value without alignment padding
                            //  but with the optional formatting code string
                            //
                            String argValue = String.Format(uiCulture,
                                     "{0"
                                     + formatPart
                                     + "}",
                                     varArgs[argV]);

                            // then pad left and right to center the value
                            //
                            if (width < 0)
                            {
                                width = -width;

                                if (argValue.Length < width)
                                {
                                    // round down for left alignment
                                    //
                                    int padding = argValue.Length
                                                + ((width - argValue.Length) / 2);
                                    argValue = argValue.PadLeft(padding).PadRight(width);
                                }
                            }
                            else
                            {
                                if (argValue.Length < width)
                                {
                                    // round up for right alignment
                                    //
                                    int padding = argValue.Length
                                                + (((width - argValue.Length) + 1) / 2);
                                    argValue = argValue.PadLeft(padding).PadRight(width);
                                }
                            }
                            // remember in varArgs this new value
                            //
                            argV = extArgs.Count;
                            extArgs.Add(argValue);

                            // replace original formatting area with the simplified
                            //  pointing to our generated argument value.
                            //
                            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
                        {
                            // replace original formatting area with the simplified
                            //  pointing to the original value.
                            //
                            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;
                        }
                    }

                    // replace original formatting area with the simplified
                    //  pointing to the original value.
                    //
                    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);
                }

                // put our extended list of arguments in place
                //  for the standard formatting call.
                //
                varArgs = extArgs.ToArray();
            }

            // now let the base String.Format do its gory work
            //
            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.

// reTotal loosely matches any legal formatting placeholder:
//
//  [0]             ==  "{...}"     ==  the entire placeholder
//  [1]                             ==  index into varArgs for the value
//  [2]             ==  ",..."      ==  optional alignment specifier
//      [3]         ==  "c"         ==  optional center alignment modifier
//      [4]         ==  "-"         ==  optional left alignment modifier
//      [5]         ==  "..."       ==  alignment width option
//          [6]     ==  "{N}"       ==  bracketed index to indirect alignment
//          [7]     ==  "N"         ==  index into varArgs for the alignment
//          [8]     ==  "N"         ==  actual value of alignment width
//                                      (not bracketed, so not indirect)
//  [9]             ==  ":..."      ==  optional formatting specifier
//      [10]        ==  "..."       ==  alignnemt width option
//          [11]    ==  "{N}"       ==  bracketed index to the
//                                      indirect formatting argument
//          [12]    ==  "N"         ==  index into varArgs for the
//                                      indirect formatting code string
//          [13]    ==  "..."       ==  actual value of formatting
//                                      (not bracketed, so not indirect)
//
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
DateVersionNotes
2012.06.131.0.0.1Tweaked in-line documentation (in the attached ZIP file).
2012.06.131.0.0.0Initial creation of FormatEx code.
FormatEx Article Revision History
DateVersionNotes
2012.06.131.0.0.1Cosmetic tweaks to adjust fro a too-smart-for-my-own-good submission wizard.
2012.06.131.0.0.0Initial 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here