一种 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 的任何项目的一部分。
我有时也担任自由职业顾问。此代码并非任何此类合同项目的一部分。