65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2012年6月14日

CPOL

6分钟阅读

viewsIcon

34477

downloadIcon

589

FormatEx 是一种允许从参数中间接构造格式占位符的方法。

引言

在标准 `String.Format()` 实现中,有几个非常方便的扩展格式化操作是无法实现的。例如,将您的值居中显示在字段中,或者从参数列表中的变量获取字段宽度。

背景

C# 间接字符串格式化的问题引起了我的注意,当时我偶然发现了这些问题

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,结构

我使用正则表达式来查找所有字段占位符,无论它们是否包含我的扩展。

如果字段占位符有任何扩展,则可能会发生两件事

  1. 字段占位符被替换为一个更简单的占位符,没有间接性。
  2. `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.131.0.0.1调整了内联文档(在附件的 ZIP 文件中)。
2012.06.131.0.0.0`FormatEx` 代码的初始创建。
FormatEx 文章修订历史
日期版本注释
2012.06.131.0.0.1对提交向导进行了一些微小的调整,以适应其过于智能的行为。
2012.06.131.0.0.0文章和演示控制台应用程序的初始创建。

免责声明

我在 American Dynamics(Tyco International 的子公司)担任软件工程师。此代码并非 American Dynamics 的任何项目的一部分。

我有时也担任自由职业顾问。此代码并非任何此类合同项目的一部分。

© . All rights reserved.