Introduction
It is common we C# developers most of the time manipulate string values in our program for various reasons especially when it comes from user input, database or files etc. For example, we convert a string to integer when a user fills age value in a text box. The String
class offers ConvertXxx
methods for conversions like this but it lacks in flexibility and control, I think . I have just consolidated all the extensions method I wrote against String class depends on the requirement at various times. Sure enough there are different ways you can achieve what I did using extension methods but I hope you will find them easy and extensible.
Background
Using built-in string methods can help us get the desired result but you repeat the code everywhere in need and I felt isolating the logic to one place will help the logic maintainable and testable.
Using the code
There are only two classes you can look at in the source code attached here : StringExtensions
and StringExtensionsTest
.The first class has all the extension methods for string class and the second class will test each extension methods with different scenarios to ensure the desired output returned to the user.
These are the 13 extension methods.
We will look at each method and few test methods to verify the result (there are 59 test methods in total which you can look at it by downloading the source code)
Note : I named the test method as Roy Osherove suggested
in his book The art of Unit Testing. That is [methodUnderTest]_[Scenario]_[Expected].
Example
public void toDate_hasValidDate_Date()
Extension methods
1.toDate
Accepts a string and convert as DateTime. It has an optional parameter throwExceptionIfFailed
. if it is true
then caller of this method should expect exception and handle it. Default is false
which means that it returns DateTime.MinValue
if the string cannot be converted as DateTime
public static DateTime toDate(this string input, bool throwExceptionIfFailed = false)
{
DateTime result;
var valid = DateTime.TryParse(input, out result);
if (!valid)
if (throwExceptionIfFailed)
throw new FormatException(string.Format("'{0}' cannot be converted as DateTime", input));
return result;
}
And the test methods:
[TestMethod]
public void toDate_hasValidDate_Date()
{
Assert.AreEqual(DateTime.Parse("2013-12-05"), "2013-12-05".toDate());
}
[TestMethod]
public void toDate_hasInvalidDate_MinDate()
{
Assert.AreEqual(DateTime.MinValue, "2013-50-70".toDate());
}
[TestMethod]
[ExpectedException(typeof(FormatException))]
public void toDate_notADateStringButWantException_FormatException()
{
"abcd".toDate(true);
}
2.toInt
public static int toInt(this string input, bool throwExceptionIfFailed = false)
{
int result;
var valid = int.TryParse(input, out result);
if (!valid)
if (throwExceptionIfFailed)
throw new FormatException(string.Format("'{0}' cannot be converted as int", input));
return result;
}
And the test methods
[TestMethod]
public void toInt_notANumber_0()
{
Assert.AreEqual(0, "abcd".toInt());
}
[TestMethod]
public void toInt_10_10()
{
Assert.AreEqual(10, "10".toInt());
}
[TestMethod]
public void toInt_alphaNumeric_0()
{
Assert.AreEqual(0, "a10b34c".toInt());
}
3.toDouble
public static double toDouble(this string input, bool throwExceptionIfFailed = false)
{
double result;
var valid = double.TryParse(input, NumberStyles.AllowDecimalPoint,
new NumberFormatInfo { NumberDecimalSeparator = "." }, out result);
if (!valid)
if (throwExceptionIfFailed)
throw new FormatException(string.Format("'{0}' cannot be converted as double", input));
return result;
}
And the test methods
[TestMethod]
public void toDouble_notANumber_0()
{
Assert.AreEqual(0, "abcd".toDouble());
}
[TestMethod]
public void toDouble_10point5_10point5()
{
Assert.AreEqual(10.5, "10.5".toDouble());
}
4.reverse
public static string reverse(this string input)
{
if (string.IsNullOrWhiteSpace(input)) return string.Empty;
char[] chars = input.ToCharArray();
Array.Reverse(chars);
return new String(chars);
}
And the test methods
[TestMethod]
public void reverse_empty_empty()
{
Assert.AreEqual("", "".reverse());
}
[TestMethod]
public void reverse_abcd_dcba()
{
Assert.AreEqual("dcba", "abcd".reverse());
}
5.toSentence
public static string toSentence(this string input)
{
if (string.IsNullOrWhiteSpace(input))
return input;
if (Regex.Match(input, "[0-9A-Z]+$").Success)
return input;
var result = Regex.Replace(input, "(\\B[A-Z])", " $1");
return result;
}
And the test methods
[TestMethod]
public void toSentence_OneCapLetter_OneSpace()
{
Assert.AreEqual("Hello World", "HelloWorld".toSentence());
}
[TestMethod]
public void toSentence_5CapLetters_5Spaces()
{
Assert.AreEqual("Such A Long Text To Test",
"SuchALongTextToTest".toSentence());
}
[TestMethod]
public void toSentence_NoCapLetter_AsIs()
{
Assert.AreEqual("test", "test".toSentence());
}
One sample usage of this method. Consider this Enum
and you print them using this extension
enum Task
{
Code,
Test,
BugFix,
ProdDep,
DbDesign,
ScreenDesign,
TDD
}
var tasks = Enum.GetValues(typeof(Task));
foreach (var task in tasks)
{
Console.WriteLine(task.ToString().toSentence());
}
Result
Code
Test
Bug Fix
Prod Dep
Db Design
Screen Design
TDD
6.getLast
public static string getLast(this string input, int howMany)
{
if (string.IsNullOrWhiteSpace(input)) return string.Empty;
var value = input.Trim();
return howMany >= value.Length ? value : value.Substring(value.Length - howMany);
}
And the test methods
[TestMethod]
public void getLast_2CharectersButAskFor3_2charecter()
{
Assert.AreEqual("ab", "ab".getLast(3));
}
[TestMethod]
public void getLast_2CharectersAndAskFor2_2charecter()
{
Assert.AreEqual("ab", "ab".getLast(2));
}
[TestMethod]
public void getLast_4CharectersAndAskFor2_last2charecter()
{
Assert.AreEqual("cd", "abcd".getLast(2));
}
7.getFirst
public static string getFirst(this string input, int howMany)
{
if (string.IsNullOrWhiteSpace(input)) return string.Empty;
var value = input.Trim();
return howMany >= value.Length ? value : input.Substring(0, howMany);
}
And the test methods
[TestMethod]
public void getFirst_2CharectersButAskFor3_2charecter()
{
Assert.AreEqual("ab", "ab".getFirst(3));
}
[TestMethod]
public void getFirst_2CharectersAndAskFor2_2charecter()
{
Assert.AreEqual("ab", "ab".getFirst(2));
}
[TestMethod]
public void getFirst_4CharectersAndAskFor2_first2charecter()
{
Assert.AreEqual("ab", "abcd".getFirst(2));
}
8.isEmail
public static bool isEmail(this string input)
{
var match = Regex.Match(input,
@"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*", RegexOptions.IgnoreCase);
return match.Success;
}
And the test methods
[TestMethod]
public void isEmail_notEmail_false()
{
Assert.AreEqual(false, "1234".isEmail());
}
[TestMethod]
public void isEmail_mail_true()
{
Assert.AreEqual(true, "abc@abc.com".isEmail());
}
9.isPhone
public static bool isPhone(this string input)
{
var match = Regex.Match(input,
@"^\+?(\d[\d-. ]+)?(\([\d-. ]+\))?[\d-. ]+\d$", RegexOptions.IgnoreCase);
return match.Success;
}
And the test methods
[TestMethod]
public void isPhone_ValidNumberPrefixedPlus_true()
{
Assert.AreEqual(true, "+1234".isPhone());
}
[TestMethod]
public void isPhone_ValidNumberPrefixedTwoPlus_false()
{
Assert.AreEqual(false, "++1234".isPhone());
}
10.isNumber
public static bool isNumber(this string input)
{
var match = Regex.Match(input, @"^[0-9]+$", RegexOptions.IgnoreCase);
return match.Success;
}
And the test methods
Sorry, no test methods! task for you:)
11.extractNumber
public static int extractNumber(this string input)
{
if (string.IsNullOrWhiteSpace(input)) return 0;
var match = Regex.Match(input, "[0-9]+", RegexOptions.IgnoreCase);
return match.Success ? match.Value.toInt() : 0;
}
And the test methods
[TestMethod]
public void extractNumber_TextWith100_100()
{
Assert.AreEqual(100, "in 100 between".extractNumber());
}
[TestMethod]
public void extractNumber_100_100()
{
Assert.AreEqual(100, "100".extractNumber());
}
[TestMethod]
public void extractNumber_TextWithMixedNumbers_firstNumber()
{
Assert.AreEqual(100, "first 100 then 60 then 8 then24".extractNumber());
}
12.extractEmail
public static string extractEmail(this string input)
{
if (input == null || string.IsNullOrWhiteSpace(input)) return string.Empty;
var match = Regex.Match(input, @"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*", RegexOptions.IgnoreCase);
return match.Success ? match.Value : string.Empty;
}
And the test methods
[TestMethod]
public void extractEmail_containsEmail_Email()
{
Assert.AreEqual("email@email.com", "name,+86738238;email@email.com;address".extractEmail());
}
[TestMethod]
public void extractEmail_noEmail_empty()
{
Assert.AreEqual("", "just some text".extractEmail());
}
[TestMethod]
public void extractEmail_Email_Email()
{
Assert.AreEqual("abc@abc.com", "abc@abc.com".extractEmail());
}
13.extractQueryStringParamValue
public static string extractQueryStringParamValue(this string queryString, string paramName)
{
if (string.IsNullOrWhiteSpace(queryString) || string.IsNullOrWhiteSpace(paramName)) return string.Empty;
var query = queryString.Replace("?", "");
if (!query.Contains("=")) return string.Empty;
var queryValues = query.Split('&').Select(piQ => piQ.Split('=')).ToDictionary(
piKey => piKey[0].ToLower().Trim(), piValue => piValue[1]);
string result;
var found = queryValues.TryGetValue(paramName.ToLower().Trim(), out result);
return found ? result : string.Empty;
}
And the test methods
[TestMethod]
public void extractQueryStringParamValue_twoParamsWantFirst_first()
{
Assert.AreEqual("One",
"?param1=OneĀ¶m2=two".extractQueryStringParamValue("param1"));
}
[TestMethod]
public void extractQueryStringParamValue_invalidQueryString_empty()
{
Assert.AreEqual("", "justfun".extractQueryStringParamValue("param1"));
}
Points of Interest
You can add/modify scenarios to the test methods to get desired output based on your requirement.For example in isPhone
, you can put restriction on min and max length.Subsequently you change the extension method if the test method fails.
History
Initial version as on 5-Dec-2013