一种 C# 实现间接宽度和样式格式化的方法。





5.00/5 (2投票s)
FormatEx 是一种允许从参数中间接构造格式占位符的方法。
引言
在标准 `String.Format()` 实现中,有几个非常方便的扩展格式化操作是无法实现的。例如,将您的值居中显示在字段中,或者从参数列表中的变量获取字段宽度。
背景
C# 间接字符串格式化的问题引起了我的注意,当时我偶然发现了这些问题
- user72491:http://stackoverflow.com/questions/644017/net-format-a-string-with-fixed-spaces
- Ken:http://stackoverflow.com/questions/2314320/take-next-parameter-as-field-width-in-string-format。
user72491 想要缺失的 `String.PadCenter(width)` 功能,Joel 为他提供了。
Ken 正在寻找一种 C# 方法来实现典型的 C 格式化操作,即使用一个参数来指定另一个参数格式化的字段宽度。
// prints 123 as "  123"
int width = 5;
int value = 123;
printf( "This is formatted: |%*s| %d wide.", width, value, width);
Ken 希望避免笨拙的字符串拼接方式
// prints 123 as "  123"
int width = 5;
int value = 123;
Console.WriteLine(
        "This is formatted: |{0, "
        + width.ToString()
        + "}| {1} wide.",
    value, width);
这种方式不仅容易出错,而且每次需要该功能时都必须以自定义的方式来实现。
提供了各种答案,并提供了指向其他类似问题和答案文章的链接。
我决定将所有答案和建议封装到一个方法中,并加入我自己的想法,使其具有通用性。
而且通用的代码是值得分享的。 ;-D
但我的回答对于在 stackoverflow 上的简单回答帖子来说太长了,因此,这篇更丰富的文章以及一个指向那里的链接。
Heath Stewart 的文章 Custom String Formatting in .NET 很好地概述了使用标准格式化字段占位符可以做的许多事情。
或者您可以查阅 MSDN 文章 Composite Formatting 以获取所有详细信息。
`FormatEx` 是一个预处理器,它将扩展格式化字段转换为一个标准的字段,以达到预期的目标。然后它调用 `String.Format()` 并返回最终结果。
使用代码
基本上,任何您会调用 `String.Format()` 的地方,都可以改用 `FormatEx()`。所有标准的格式化字段描述仍然有效。
标准格式化占位符
标准的格式化占位符如下所示(有关所有详细信息,请参阅 MSDN 文章 Composite Formatting)
{index,alignment:formatString}
其中
| index | 是格式化值的零基索引。 | ||
| , 对齐方式 | 是一个可选的正整数,用于右对齐,或一个负整数,用于左对齐。 | ||
| : formatString | 是一个可选字符串,描述格式的样式。 | 
扩展格式化占位符
Ken 希望看到的扩展可能如下所示
{index1,{index2}}
{index1,{index2}:formatString}
然而,由于我将在本文中添加三个扩展,它也可能如下所示
{index1:{index3}}
{index1,{index2}:{index3}}
{index1,alignment:{index3}}
{index1,c{index2}:formatString}
{index1,c alignment:{index3}}
{index1,c alignment:formatString}
{index1,c{index2}:{index3}}
其中
| index1 | 是格式化值的零基索引。 | ||
| c | 是一个可选标志,用于指示中心对齐 | ||
| index2 | 是对齐宽度值的零基索引。 | ||
| index3 | 是对格式化样式描述字符串的零基索引。 | ||
| alignment | 是一个可选的正整数,用于右对齐,或一个负整数,用于左对齐。 | ||
| formatString | 是一个可选字符串,描述格式的样式。 | 
居中对齐
将值居中在宽度字段中有几个棘手的地方
- 如果您还指定了 `formatString`。
 必须首先将 `formatString` 应用于该值,然后将格式化后的结果居中放在宽度中。
- 如果需要居中的值需要奇数个填充空格来填满宽度。
 由于对齐通常对正值执行右对齐,对负值执行左对齐,我认为这是调整中心填充所需的线索。
- 如果同一个参数值需要在同一调用中以多种方式格式化。
 必须为每次特殊格式化使用原始值。
 例如Console.WriteLine("|{0,{1}}| and |{0,{2}}|", 123, 5, -5); // expect: | 123| and |123 |
间接对齐
使用嵌套的 {index2} 来帮助我获取最终的对齐说明符,这只需要一些解析和数组索引。
正则表达式让我知道我是否有一个指向对齐宽度的间接索引,或者我是否有一个标准的对齐宽度。
间接格式化代码字符串
同样,嵌套的 {index3} 允许我检索传入的 `formatString` 以用于该字段。
正则表达式让我知道我是否有一个指向 `formatString` 的间接索引,或者我是否有一个标准的 `formatString`。
FormatEx,结构
我使用正则表达式来查找所有字段占位符,无论它们是否包含我的扩展。
如果字段占位符有任何扩展,则可能会发生两件事
- 字段占位符被替换为一个更简单的占位符,没有间接性。
- `params Object[] varArgs` 中的参数会按要求格式化,并且新值将被追加到参数列表的末尾。
然后,最后,将简化的格式字符串和调整后的参数数组传递给 `String.Format` 进行最终格式化。
因为 `String.Format` 有一个带有 `CultureInfo` 参数的版本,所以我也为 `FormatEx` 定义了一个。我不想费力定义像 `String.Format` 那样的所有其他参数签名,因为在我看来,`params Object[] varArgs` 已经包含了所有内容。
FormatEx,示例调用
这里有一些示例调用来展示可以做什么。
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   |");
输出应如下所示
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,代码
// 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
}
关注点
这个项目让我再次认识到正则表达式的强大。我最初工作的代码有三个不同的表达式,分别对应一种扩展样式。然后在测试过程中,我发现单独处理它们并不利于同时使用两个或三个扩展。
新的组合正则表达式匹配所有合法的标准或扩展字段格式化占位符。并且我已经掌握了所有解析出的信息,可以以最佳方式将其重新组合。
在此再次展示它,尽情欣赏。
// 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+)})|([^}]+)))?}");
结论
好了,就是这样。一个功能类似 `String.Format`,但带有一些嵌套扩展的方法。
它可能是一个内存消耗大户,因为它在需要格式化时会复制参数数组。您的使用情况可能不同。我最初是在 `varArgs` 数组中替换值。但当调用者在多个占位符中使用相同的值但格式化不同时,这会彻底失败。
例如
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   |");
生成
Value     = ||      3.14|3.142     |   3.14   ||
Expecting = ||      3.14|3.142     |   3.14   ||
我希望您觉得本文和代码有用,或者至少具有启发性。
历史
| FormatEx 代码修订历史 | ||
|---|---|---|
| 日期 | 版本 | 注释 | 
| 2012.06.13 | 1.0.0.1 | 调整了内联文档(在附件的 ZIP 文件中)。 | 
| 2012.06.13 | 1.0.0.0 | `FormatEx` 代码的初始创建。 | 
| FormatEx 文章修订历史 | ||
| 日期 | 版本 | 注释 | 
| 2012.06.13 | 1.0.0.1 | 对提交向导进行了一些微小的调整,以适应其过于智能的行为。 | 
| 2012.06.13 | 1.0.0.0 | 文章和演示控制台应用程序的初始创建。 | 
免责声明
我在 American Dynamics(Tyco International 的子公司)担任软件工程师。此代码并非 American Dynamics 的任何项目的一部分。
我有时也担任自由职业顾问。此代码并非任何此类合同项目的一部分。


