有时候,我们需要一种基于名称的格式化方法,而不是基于位置的格式化,String.Format方法就是基于位置的格式化方法,使用方法如下:
string s = string.Format("{0} first, {1} second", 3.14, DateTime.Now);
基于位置的格式化方法在位置变动的时候,修改起来很麻烦。而且可读性也不好。所以我们可能需要如下格式化方法:
Dictionary<string, object> testDic = new Dictionary<string, object>
{ { "TestKey", "Helloworld" },
{ "foo" , 123.45 },
{ "bar" , 42},
{ "date", date}
};
string result = StringFormatter.Format("TestKey is {TestKey}", testDic);
从某种意义上来说, 第二种方案显得更清晰。完整的实现代码和UnitTest代码如下:
public class StringFormatter
{
/// <summary>
/// Replaces the format item in a specified String with the text equivalent of the value of a corresponding Object instance in a specified array.
/// Using key name in the format string to refer a object in the args dictionary with the same key
/// </summary>
/// <param name="format">A composite format string. </param>
/// <param name="args">A dictionary containing zero or more objects to format.</param>
/// <returns>A copy of format in which the format items have been replaced by the String equivalent of the corresponding instances of Object in args.</returns>
public static string Format(string format, Dictionary<string, object> args)
{
if ((format == null) || (args == null))
{
throw new ArgumentNullException((format == null) ? "format" : "args");
}
StringBuilder strBuilder = new StringBuilder(format.Length + args.Count * 8);
StringBuilder expBuilder = new StringBuilder(20);
StringBuilder keyBuilder = new StringBuilder(20);
char[] chArray = format.ToCharArray();
bool hasFoundStartBrace = false;
bool hasGotKeyName = false;
for (int i = 0; i < chArray.Length; i++)
{
if (!hasFoundStartBrace)
{
if (chArray[i] == '{')
{
if (i + 1 == chArray.Length)
{
ThrowFormatException();
}
else if (chArray[i + 1] == '{')
{
strBuilder.Append(chArray[i]);
i++;
}
else
{
hasFoundStartBrace = true;
expBuilder.Append(chArray[i]);
}
}
else if(chArray[i] == '}')
{
if (i + 1 == chArray.Length)
{
ThrowFormatException();
}
else if (chArray[i + 1] == '}')
{
strBuilder.Append(chArray[i]);
i++;
}
else
{
ThrowFormatException();
}
}
else
{
strBuilder.Append(chArray[i]);
}
}
else
{
if (chArray[i] == '}')
{
if (!hasGotKeyName)
{
hasGotKeyName = true;
expBuilder.Append('0');
}
expBuilder.Append('}');
object value;
string keyName = keyBuilder.ToString();
if (!args.TryGetValue(keyName, out value))
{
throw new KeyNotFoundException(string.Format("Key '{0}' not found", keyName));
}
strBuilder.AppendFormat(expBuilder.ToString(), value);
expBuilder.Remove(0, expBuilder.Length);
keyBuilder.Remove(0, keyBuilder.Length);
hasFoundStartBrace = false;
hasGotKeyName = false;
}
else
{
if (hasGotKeyName)
{
expBuilder.Append(chArray[i]);
}
else
{
if (chArray[i] != ',' && chArray[i] != ':')
{
keyBuilder.Append(chArray[i]);
}
else
{
expBuilder.Append('0');
expBuilder.Append(chArray[i]);
hasGotKeyName = true;
}
}
}
}
}
if (hasFoundStartBrace)
{
ThrowFormatException();
}
return strBuilder.ToString();
}
private static void ThrowFormatException()
{
throw new FormatException("Input string was not in a correct format.");
}
}
/// <summary>
///This is a test class for StringFormatterTest and is intended
///to contain all StringFormatterTest Unit Tests
///</summary>
[TestClass()]
public class StringFormatterTest
{
private TestContext testContextInstance;
/// <summary>
///Gets or sets the test context which provides
///information about and functionality for the current test run.
///</summary>
public TestContext TestContext
{
get
{
return testContextInstance;
}
set
{
testContextInstance = value;
}
}
#region Additional test attributes
//
//You can use the following additional attributes as you write your tests:
//
//Use ClassInitialize to run code before running the first test in the class
[ClassInitialize()]
public static void ClassInitialize(TestContext testContext)
{
date = DateTime.Now;
testDic = new Dictionary<string, object>
{ { "TestKey", "Helloworld" },
{ "foo" , 123.45 },
{ "bar" , 42},
{ "date", date}
};
}
//
//Use ClassCleanup to run code after all tests in a class have run
//[ClassCleanup()]
//public static void MyClassCleanup()
//{
//}
//
//Use TestInitialize to run code before running each test
//[TestInitialize()]
//public void MyTestInitialize()
//{
//}
//
//Use TestCleanup to run code after each test has run
//[TestCleanup()]
//public void MyTestCleanup()
//{
//}
//
#endregion
private static DateTime date;
private static Dictionary<string, object> testDic;
/// <summary>
/// Format an empty string, the result should be empty and not throw an exception
/// </summary>
[TestMethod()]
public void FormatEmptyString()
{
string result = StringFormatter.Format(string.Empty, testDic);
Assert.AreEqual<string>(string.Empty, result);
}
/// <summary>
/// Formatting a string contains no key should get the same string
/// </summary>
[TestMethod()]
public void NoKeyReferToDictionary()
{
string result = StringFormatter.Format("TestKey", testDic);
Assert.AreEqual<string>("TestKey", result);
}
/// <summary>
/// Escape leading brace at the begining position in format string
/// </summary>
[TestMethod()]
public void EscapeLeadingBraceAtBegining()
{
string result = StringFormatter.Format("{{TestKey", testDic);
Assert.AreEqual<string>("{TestKey", result);
}
/// <summary>
/// Escape leading brace at the end position in format string
/// </summary>
[TestMethod()]
public void EscapeLeadingBraceAtEnd()
{
string result = StringFormatter.Format("TestKey{{", testDic);
Assert.AreEqual<string>("TestKey{", result);
}
/// <summary>
/// Escape trailing brace at the begining position in format string
/// </summary>
[TestMethod()]
public void EscapeTrailingBraceAtBegining()
{
string result = StringFormatter.Format("}}TestKey", testDic);
Assert.AreEqual<string>("}TestKey", result);
}
/// <summary>
/// Escape trailing brace at the end position in format string
/// </summary>
[TestMethod()]
public void EscapeTrailingBraceAtEnd()
{
string result = StringFormatter.Format("TestKey}}", testDic);
Assert.AreEqual<string>("TestKey}", result);
}
/// <summary>
/// Escape both leading brace and trailing brace in format string for one time
/// </summary>
[TestMethod()]
public void EscapeLeadingBraceAndTrailingBraceOnce()
{
string result = StringFormatter.Format("{{TestKey}}", testDic);
Assert.AreEqual<string>("{TestKey}", result);
}
/// <summary>
/// Escape both leading brace and trailing brace in format string for two times
/// </summary>
[TestMethod()]
public void EscapeLeadingBraceAndTrailingBraceTwice()
{
string result = StringFormatter.Format("{{{{TestKey}}}}", testDic);
Assert.AreEqual<string>("{{TestKey}}", result);
}
/// <summary>
/// Format string value referrd by a key contained in format string
/// </summary>
[TestMethod()]
public void FormatStringValue()
{
string result = StringFormatter.Format("begin-{TestKey}-end", testDic);
Assert.AreEqual<string>("begin-Helloworld-end", result);
}
/// <summary>
/// Format string value and Escape both leading brace and trailing brace once
/// </summary>
[TestMethod()]
public void EscapeLeadingBraceAndTrailingBraceOnce_FormatStringValue()
{
string result = StringFormatter.Format("{{{TestKey}}}", testDic);
Assert.AreEqual<string>("{Helloworld}", result);
}
/// <summary>
/// Format string value and Escape both leading brace and trailing brace twice
/// </summary>
[TestMethod()]
public void EscapeLeadingBraceAndTrailingBraceTwice_FormatStringValue()
{
string result = StringFormatter.Format("{{{{{TestKey}}}}}", testDic);
Assert.AreEqual<string>("{{Helloworld}}", result);
}
/// <summary>
/// Test formatted string's alignment
/// </summary>
[TestMethod()]
public void FormatStringValue_TestAlignment()
{
string result = StringFormatter.Format("{TestKey,20}", testDic);
Assert.AreEqual<string>(" Helloworld", result);
}
/// <summary>
/// Format date value
/// </summary>
[TestMethod()]
public void FormatDateValue()
{
string result = StringFormatter.Format("{date:yyyyMMdd}", testDic);
Assert.AreEqual<string>(date.ToString("yyyyMMdd"), result);
}
/// <summary>
/// Format date value and Test formatted string's alignment
/// </summary>
[TestMethod()]
public void FormatDateValue_TestAlignment()
{
string result = StringFormatter.Format("{date,20:yyyyMMdd}", testDic);
Assert.AreEqual<string>(new string(' ', 12) + date.ToString("yyyyMMdd"), result);
}
/// <summary>
/// Format multiple values
/// </summary>
[TestMethod()]
public void FormatMultipleValues()
{
string result = StringFormatter.Format("{foo} {foo} {bar}{TestKey}", testDic);
Assert.AreEqual<string>("123.45 123.45 42Helloworld", result);
}
/// <summary>
/// Format numeric value
/// </summary>
[TestMethod()]
public void FormatNumericValue()
{
string result = StringFormatter.Format("{foo:#.#}", testDic);
Assert.AreEqual<string>("123.5", result);
}
/// <summary>
/// Test throwing FormatException
/// </summary>
[TestMethod]
[ExpectedException(typeof(FormatException))]
public void ExceptionTest_FormatException()
{
string result = StringFormatter.Format("{{T}", new Dictionary<string, object>());
}
/// <summary>
/// Test throwing FormatException
/// </summary>
[TestMethod]
[ExpectedException(typeof(FormatException))]
public void ExceptionTest_OnlyOneLeadingBrace()
{
string result = StringFormatter.Format("{", new Dictionary<string, object>());
}
/// <summary>
/// Test throwing FormatException
/// </summary>
[TestMethod]
[ExpectedException(typeof(FormatException))]
public void ExceptionTest_OnlyOneTrailingBrace()
{
string result = StringFormatter.Format("}", new Dictionary<string, object>());
}
/// <summary>
/// Test throwing FormatException
/// </summary>
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ExceptionTest_ArugumentNull()
{
string result = StringFormatter.Format(null, null);
}
/// <summary>
/// Test throwing KeyNotFoundException
/// </summary>
[TestMethod]
[ExpectedException(typeof(KeyNotFoundException))]
public void ExceptionTest_KeyNotFound()
{
string result = StringFormatter.Format("{inexistencekey:29}", new Dictionary<string, object>());
}
}