ApplyFormat






4.33/5 (2投票s)
将多个格式应用于值的 a method
背景
对于我的 GenOmatic,我希望用户能够提供一个格式来输出数据。为此,我想使用 value.ToString(format)
,从而允许用户提供任何适合数据类型的格式,而无需实用程序了解该数据类型的任何信息。这对于 ToString
支持的简单格式来说效果很好,但我想支持更复杂的格式。
示例:以十六进制打印整数值,并带有前导“0x”。理想情况下,我将使用格式为“'0x'Xn”(其中 n 表示宽度)。这无法通过提供的 int.ToString(format)
方法实现;您可以使用包含“Xn”但不包含字面文本的“标准数字格式字符串”,或者 包含字面文本但不包含十六进制数字的“自定义数字格式字符串”。
支持更复杂格式的一种方法是改用 string.Format
;string.Format(format, value)
,在这种情况下,格式可以是“0x{0:Xn}”。此解决方案有效,但它要求用户提供花括号等内容;用户不应该需要了解这些。
我想要的是一种方法,允许用户指定“'0x'Xn”格式,将字面文本与格式文本分开,并单独输出它们。拆分提供的格式字符串还允许值被格式化到输出中不止一次,这同样是 ToString
不支持的。下面是一个提供此功能的扩展方法。
在我发布本文的第一个版本后,C# 论坛中出现了一个问题,询问如何左填充值,但让符号(+ 或 -)出现在填充之前。这无法使用 string.Format
或 WriteLine
的单个格式来实现,因此我着手为该方法添加此功能。
LibExt.ApplyFormat.cs
正则表达式
该方法使用以下正则表达式将提供的格式字符串拆分为字面文本和非字面文本的子字符串,假设非字面文本是填充/对齐信息和有效格式字符串。
private static readonly System.Text.RegularExpressions.Regex regex =
new System.Text.RegularExpressions.Regex
(
"'(?'Text'[^']*)'|" +
"\"(?'Text'[^\"]*)\"|" +
"(?'IsFormat'((?'Justify'[+\\-/]?)(?'MinWidth'\\d{0,4})" +
"(,(?'MaxWidth'\\d{0,4})(,(?'PadChar'.?))?)?:)?(?'Format'[^'\"]*))"
,
System.Text.RegularExpressions.RegexOptions.CultureInvariant
) ;
(如果任何人对该正则表达式有任何意见或改进建议,请告诉我。)
关于填充/对齐的说明
格式现在可以根据以下格式添加填充信息前缀
[ Justify ] [ MinWidth ] [ , [ MaxWidth] [ , [ PadChar ] ] :
对齐
指定在哪里添加填充字符的字符
- 加号(+)表示右对齐(左填充);这是默认设置
- 减号(-)表示左对齐(右填充)
- 斜杠(/)表示居中;如果需要奇数个填充字符,则多出的一个将附加到末尾。
最小宽度
零到四位十进制数字,用于指定格式化值的最小宽度,必要时会将格式化值填充到此长度;默认值为 0。
最大宽度
零到四位十进制数字,用于指定格式化值的最大宽度,必须以逗号(,)开头;必要时会将格式化值截断到此长度;默认值为 int.MaxValue
。
填充字符
用于填充的字符,必须以逗号(,)开头;默认值为空格。
:
填充信息以冒号(:)终止
例如
“/10,,=:0”-- 将值居中,放在至少十个字符的 string
中,并用等号(=)填充;将其应用于 123 会得到“===123===="
使用 XML
在我完成了所有这些工作之后,我决定添加支持,通过 XML 指定格式和填充/对齐信息……纯粹是出于兴趣。
元素的名称必须是 Formatter,Format
放在元素的 قيمة 中,并且支持以下 Attributes
对齐
在哪里添加填充字符
- 单词“
Right
”或加号(+)表示右对齐(左填充);这是默认设置 - 单词“
Left
”或减号(-)表示左对齐(右填充) - 单词“
Center
”或斜杠(/)表示居中;如果需要奇数个填充字符,则多出的一个将附加到末尾。
最小宽度
指定格式化值的最小宽度的十进制数字,必要时会将格式化值填充到此长度;默认值为 0。
最大宽度
指定格式化值的最大宽度的十进制数字,必要时会将格式化值截断到此长度;默认值为 int.MaxValue
。
PadChar
用于填充的字符;默认值为空格。
上面的示例可以用 XML 编写如下
<Formatter Justify="Center" MinWidth="10" PadChar="=">0</Formatter>
解释助手
以下定义有助于解释的缓存
private interface IFormat
{
string Format ( object Value , System.IFormatProvider FormatProvider ) ;
}
private sealed class FormatList : System.Collections.Generic.List<IFormat> {}
private sealed class FormatDictionary : System.Collections.Generic.Dictionary {}
private static readonly FormatDictionary knownformats = new FormatDictionary() ;
扩展方法
此方法的第一版非常简单,但现在它必须解释填充/对齐信息。我选择一次性执行该解释并缓存结果。它还需要检测 XML。我稍后会详细介绍。
此方法的 string
版本执行以下操作
- 如果
Value
为null
,则抛出ArgumentNullException
- 如果没有提供
Format
,则返回Value
的简单ToString
- 如果我们没有在缓存中找到
Format
,则解释它并将其添加到缓存中 - 对于解释后的
format
的每个部分,将Format
的结果附加到结果中
public static string
ApplyFormat
(
this object Value
,
string Format
,
System.IFormatProvider FormatProvider
)
{
if ( Value == null )
{
throw ( new System.ArgumentNullException
(
"Value"
,
"Value must not be null"
) ) ;
}
if ( string.IsNullOrEmpty ( Format ) )
{
return ( Value.ToString() ) ;
}
if ( !knownformats.ContainsKey ( Format ) )
{
knownformats [ Format ] = InterpretFormat ( Format ) ;
}
System.Text.StringBuilder result = new System.Text.StringBuilder() ;
foreach ( IFormat format in knownformats [ Format ] )
{
result.Append ( format.Format
(
Value
,
FormatProvider
) ) ;
}
return ( result.ToString() ) ;
}
此方法的 XML 处理重载非常相似,它执行以下操作
- 如果
Value
为null
,则抛出ArgumentNullException
- 如果没有提供
Format
,则返回Value
的简单ToString
- 如果我们没有在缓存中找到
Format
,则解释它并将其添加到缓存中 - 对于解释后的
format
的每个部分,将Format
的结果附加到结果中
public static string
ApplyFormat
(
this object Value
,
System.Xml.XmlElement Format
,
System.IFormatProvider FormatProvider
)
{
if ( Value == null )
{
throw ( new System.ArgumentNullException
(
"Value"
,
"Value must not be null"
) ) ;
}
if ( ( Format == null ) || ( Format.ChildNodes.Count == 0 ) )
{
return ( Value.ToString() ) ;
}
string temp = Format.OuterXml ;
if ( !knownformats.ContainsKey ( temp ) )
{
knownformats [ temp ] = InterpretFormat ( Format ) ;
}
System.Text.StringBuilder result = new System.Text.StringBuilder() ;
foreach ( IFormat format in knownformats [ temp ] )
{
result.Append ( format.Format
(
Value
,
FormatProvider
) ) ;
}
return ( result.ToString() ) ;
}
是的,您没有看错;我有两个 return
语句,我认为这种情况是合理的。
这些方法还有不需要 IFormatProvider
的重载。
InterpretFormat
InterpretFormat
有两个重载;一个用于 string
,一个用于 XML。
string
版本必须检查 XML;如果 string
包含格式正确的 XML,则将生成的 XmlElement
传递给 XML 版本。否则,将执行常规的正则表达式匹配,并将适当的 IFormat
实例添加到 FormatList
中。
private static FormatList
InterpretFormat
(
string Format
)
{
FormatList result = null ;
try
{
System.Xml.XmlDocument doc = new System.Xml.XmlDocument() ;
doc.LoadXml ( Format ) ;
result = InterpretFormat ( doc.DocumentElement ) ;
}
catch ( System.Xml.XmlException )
{
result = new FormatList() ;
foreach
(
System.Text.RegularExpressions.Match mat
in
regex.Matches ( Format )
)
{
if ( mat.Groups [ "IsFormat" ].Value.Length != 0 )
{
result.Add ( new Formatter ( mat ) ) ;
}
else
{
if ( mat.Groups [ "Text" ].Value.Length != 0 )
{
result.Add ( new Text ( mat ) ) ;
}
}
}
}
return ( result ) ;
}
XML 版本解释提供的 Format
元素的子元素
private static FormatList
InterpretFormat
(
System.Xml.XmlElement Format
)
{
FormatList result = new FormatList() ;
foreach
(
System.Xml.XmlElement ele
in
Format.ChildNodes
)
{
switch ( ele.Name )
{
case "Formatter" :
{
result.Add ( new Formatter ( ele ) ) ;
break ;
}
case "Text" :
{
if ( ele.InnerText.Length != 0 )
{
result.Add ( new Text ( ele ) ) ;
}
break ;
}
default :
{
throw ( new System.InvalidOperationException
( "Unrecognized format type: " + ele.OuterXml ) ) ;
}
}
}
return ( result ) ;
}
Formatter
Formatter
类存储解释后的格式和填充/对齐信息,并将这些设置应用于 Value
。它有点长,所以我将分块呈现。
接受 RegularExpressions.Match
的构造函数可以假定所有 Groups 都存在,但可能为空。
private sealed class Formatter : IFormat
{
private enum Justify
{
Right
,
Left
,
Center
}
private readonly Justify justify ;
private readonly int min ;
private readonly int max ;
private readonly char padchar ;
private readonly string format ;
public Formatter
(
System.Text.RegularExpressions.Match Match
)
{
switch ( Match.Groups [ "Justify" ].Value )
{
case "-" :
{
this.justify = Justify.Left ;
break ;
}
case "/" :
{
this.justify = Justify.Center ;
break ;
}
default :
{
this.justify = Justify.Right ;
break ;
}
}
int.TryParse ( Match.Groups [ "MinWidth" ].Value , out this.min ) ;
if ( !int.TryParse ( Match.Groups [ "MaxWidth" ].Value , out this.max ) )
{
this.max = int.MaxValue ;
}
this.padchar = Match.Groups [ "PadChar" ].Value.Length == 0
? ' '
: Match.Groups [ "PadChar" ].Value [ 0 ] ;
this.format = Match.Groups [ "Format" ].Value ;
return ;
}
接受 XmlElement
的构造函数需要检查 Attributes
,然后再解释 Value
。
public Formatter
(
System.Xml.XmlElement Element
)
{
if ( Element.Attributes [ "Justify" ] != null )
{
switch ( Element.Attributes [ "Justify" ].Value )
{
case "-" :
case "Left" :
{
this.justify = Justify.Left ;
break ;
}
case "/" :
case "Center" :
{
this.justify = Justify.Center ;
break ;
}
default :
{
this.justify = Justify.Right ;
break ;
}
}
}
else
{
this.justify = Justify.Right ;
}
if ( Element.Attributes [ "MinWidth" ] != null )
{
int.TryParse ( Element.Attributes [ "MinWidth" ].Value , out this.min ) ;
}
else
{
this.min = 0 ;
}
if ( Element.Attributes [ "MaxWidth" ] != null )
{
if ( !int.TryParse
( Element.Attributes [ "MaxWidth" ].Value , out this.max ) )
{
this.max = int.MaxValue ;
}
}
else
{
this.max = int.MaxValue ;
}
if ( Element.Attributes [ "PadChar" ] != null )
{
this.padchar = Element.Attributes [ "PadChar" ].Value.Length == 0
? ' '
: Element.Attributes [ "PadChar" ].Value [ 0 ] ;
}
else
{
this.padchar = ' ' ;
}
this.format = Element.InnerText ;
return ;
}
Format
方法使用存储的值来应用请求的格式。
- 确定
Value
是否为IFormattable
,并在Value
上执行适当的ToString()
重载。 - 如果生成的
string
比指定的MinWidth
短,则执行请求的填充/对齐。 - 如果生成的
string
比指定的MaxWidth
长,则将其截断。
public string
Format
(
object Value
,
System.IFormatProvider FormatProvider
)
{
string result ;
if
(
( Value is System.IFormattable )
&&
( this.format.Length != 0 )
)
{
result = ( (System.IFormattable) Value).ToString
(
this.format
,
FormatProvider
) ;
}
else
{
result = Value.ToString() ;
}
if ( result.Length < this.min )
{
switch ( this.justify )
{
case Justify.Left :
{
result = result.PadRight
(
this.min
,
this.padchar
) ;
break ;
}
case Justify.Center :
{
result = result.PadRight
(
this.min - ( this.min - result.Length ) / 2
,
this.padchar
).PadLeft
(
this.min
,
this.padchar
) ;
break ;
}
default :
{
result = result.PadLeft
(
this.min
,
this.padchar
) ;
break ;
}
}
}
if ( result.Length > this.max )
{
result = result.Substring
(
0
,
this.max
) ;
}
return ( result ) ;
}
}
文本
Text
是一个更简单的类,它只需要存储来自 Match
或 XmlElement
的文本并将其回显。
private sealed class Text : IFormat
{
private readonly string text ;
public Text
(
System.Text.RegularExpressions.Match Match
)
{
this.text = Match.Groups [ "Text" ].Value ;
return ;
}
public Text
(
System.Xml.XmlElement Element
)
{
this.text = Element.InnerText ;
return ;
}
public string
Format
(
object Value
,
System.IFormatProvider FormatProvider
)
{
return ( this.text ) ;
}
}
关于 XML 要注意的一点是,如果元素的 قيمة 只包含空格,您需要为其提供 xml:space="preserve"
属性。
例如
<Text xml:space="preserve"> </Text>
FormatProvider.cs
应用自定义格式化值的另一种方法是定义一个实现 ICustomFormatter
和 IFormatProvider
的类。有关更多信息,请参阅此链接。
以下是这样的一个类,它只是调用上述方法。
public class
FormatProvider : System.IFormatProvider , System.ICustomFormatter
{
public object
GetFormat
(
System.Type Service
)
{
return ( ( Service == typeof(System.ICustomFormatter) ) ? this : null ) ;
}
public string
Format
(
string Format
,
object Value
,
System.IFormatProvider Provider
)
{
return ( Value.ApplyFormat ( Format , Provider ) ) ;
}
}
但是,我不打算过多地使用此类;它实际上只对 string.Format
有用,如果您正在使用它,您可以使用其他格式。
Using the Code
zip 文件包含上述两个文件以及 ApplyFormatTest.cs,它从命令行获取一个整数值和一个格式 string
,并尝试使用上述类执行格式化。
int val ;
if ( int.TryParse ( args [ 0 ] , out val ) )
{
System.Console.WriteLine ( val.ApplyFormat ( args [ 1 ] ) ) ;
System.Console.WriteLine ( string.Format
( new PIEBALD.Types.FormatProvider() , "{0:" + args [ 1 ] + "}" , val ) ) ;
}
我一直在使用以下工具构建此
csc ApplyFormatTest.cs LibExt.ApplyFormat.cs FormatProvider.cs
以下是使用 ApplyFormatTest
的两个示例,它们产生相同的输出。
C:\Projects\CodeProject\Temp>ApplyFormatTest 123 "'<'/10,,=:0'>'"
<===123====>
<===123====>
C:\Projects\CodeProject\Temp>ApplyFormatTest 123 "<Format><Text>
<</Text><Formatter Justify='Center' MinWidth='10' PadChar='='>0</Formatter>
<Text>></Text></Format>"
<===123====>
<===123====>
显然,XML 版本更冗长,但如果您碰巧将格式包含在 XML 文件中,这可能是一种合理的方法。
结论
所呈现的方法实现了期望的结果,同时提供了比必需的更多的灵活性。ApplyFormat
可以在许多不将格式 string
硬编码到应用程序中的情况下使用。格式 string
可以存储在配置文件、数据库中,用户可以从下拉列表中选择一个,等等。它可以比 ToString
允许的更复杂,但又比 string.Format
的等价物更简单。
历史
- 2009-02-01 首次提交
- 2009-03-06 添加了对填充/对齐和 XML 的支持