Hugoware

The product of a web developer with a little too much caffeine

More Control When Parsing Values

with 4 comments

As much as you might try, sooner or later you’re going to wind up parsing string values and converting them into their correct type. It is… inevitable.

If you’re going for clarity then this only takes a couple lines of code to accomplish. For example…

int value = default(int);
int.TryParse("44", out value);

No big deal, in fact it is possible to do on a single line of code…

int value = int.TryParse("44", out value) ? value : default(int);

Of course, this line of code is going to draw more questions from some of the less experienced developers on your team, but hey… doesn’t it just scream ‘Alpha Programmer’?

Parsing integers isn’t really a big deal in itself but wouldn’t it be nice if the entire process could be done on one line with one method and allow you to supply default values… and without causing confusion?

Below is an example of how you might create a class that handles parsing for you.

/// <summary>
/// Handles parsing string values to appropriate types
/// </summary>
public static class Parse {

	#region Static Constructor

	//prepares the Parse class
	static Parse() {
		Parse._CustomParsing = new Dictionary<Type, Func<string, object>>();
	}

	#endregion

	#region Properties

	//holds custom parse methods
	private static Dictionary<Type, Func<string, object>> _CustomParsing;

	#endregion

	#region Public Methods

	/// <summary>
	/// Registers a custom parsing method
	/// </summary>
	public static void RegisterType<T>(Func<string, object> compare) {
		Type type = typeof(T);
		Parse._CustomParsing.Remove(type);
		Parse._CustomParsing.Add(type, compare);
	}

	/// <summary>
	/// Attempts to parse a value and return the result
	/// but falls back to the default value if the conversion fails
	/// </summary>
	public static T TryParse<T>(string value) {
		return Parse.TryParse(value, default(T));
	}

	/// <summary>
	/// Attempts to parse a value and falls back to the
	/// provided default value if the conversion fails
	/// </summary>
	public static T TryParse<T>(string value, T @default) {
		value = (value ?? string.Empty).ToString();
		Type type = typeof(T);

		//so much can go wrong here, just default to the
		//fall back type if the conversions go bad
		try {

			//perform custom parsing first
			if (Parse._PerformCustomParse(type, value, ref @default)) return @default;

			//check if this is a nullable and if we should use a child type
			//this might not work with VB since the nullable name could be different
			if (type.IsGenericType && type.Name.StartsWith("Nullable`")) {
				
				//underlying type for a nullable appears to be the first argument
				type = type.GetGenericArguments().FirstOrDefault();
				
				//if no type was found then five up
				if (type == null) { return @default; }

				//try custom parsing with the underlying type if this was a nullable
				if (Parse._PerformCustomParse(type, value, ref @default)) return @default;
			}

			//try the remaining parsing methods
			if (type.IsEnum && Parse._PerformEnumParse(type, value, ref @default)) return @default;
			if (Parse._PerformParse(type, value, ref @default)) return @default;

			//finally, just try a conversion
			Parse._PerformConvert(type, value, ref @default);
			return @default;

		}
		//settle for the default
		catch {
			return @default;
		}

        }

	#endregion

	#region Checking Values

	//uses custom parsing methods
	private static bool _PerformCustomParse<T>(Type with, string value, ref T result) {

		//if there is no custom type, cancel
		if (!Parse._CustomParsing.ContainsKey(with)) { return false; }

		//find the conversion
		Func<string, object> parse = Parse._CustomParsing[with];

		//attempt to parse
		try {
			object converted = parse(value);
			bool success = converted is T;
			if (success) { result = (T)converted; }
			return success;
		}
		//if the attempt failed
		catch {
			return false;
		}

	}

	//tries to parse using an Enum
	private static bool _PerformEnumParse<T>(Type with, string value, ref T result) {

		//check for a result
		try {
			object parsed = Enum.Parse(with, value, true);
			bool success = parsed is T;
			if (success) { result = (T)parsed; }
			return success;
		}
		catch {
			return false;
		}

	}

	//searches for a 'Parse' method
	private static bool _PerformParse<T>(Type with, string value, ref T result) {

		//make sure a try parse was even found
		MethodInfo method = with.GetMethods().FirstOrDefault(item =>
			item.Name.Equals("parse", StringComparison.OrdinalIgnoreCase) &&
			item.IsStatic);
		if (method == null) { return false; }

		//check for a result
		try {
			object parsed = method.Invoke(null, new object[] { value, result });
			bool success = parsed is T;
			if (success) { result = (T)parsed; }
			return success;
		}
		catch {
			return false;
		}

	}

	//performs common conversions
	private static bool _PerformConvert<T>(Type type, string value, ref T result) {
		object convert = Convert.ChangeType(value, type);
		bool success = convert is T;

		//update the type if needed
		if (success) { result = (T)convert; }
		return success;
	}

	#endregion

}

The general idea behind this code is to attempt to parse using a variety of ways and then fall back to a default value if nothing works out. This way we can get in and out of parsing values without needing to split up the code.

There is an ugly little bit of reflection in there, so if your application can’t incur that kind of penalty hit you might want to fall back to the classic parsing methods.

int count = Parse.TryParse("44", 0); // 44
bool success = Parse.TryParse("cheesecake", true); // true
decimal = Parse.TryParse("3.01", 3M); // 3.01

Additionally, this code allows you to add your own parsing methods to the class and use the same method across the entire project.

//register our own parsing method
Parse.RegisterType<Color>(value => ColorTranslator.FromHtml(value));
Parse.RegisterType<decimal>(value => {
    value = Regex.Replace(value, @"[^0-9\.]", string.Empty);
    return decimal.Parse(value);
    });

//use it just like any other value
Color color = Parse.TryParse("#ff0000", Color.Blue);
decimal total = Parse.TryParse("$45.33", 0M);

You could take the extension methods a step further by adding the same functionality to your strings. For example.

/// <summary>
/// Extensions to quickly parse string types
/// </summary>
public static class StringExtensions {

	/// <summary>
	/// Parses a string to the correct type or default value
	/// </summary>
	public static T ToType<T>(this string value) {
		return value.ToType(default(T));
	}

	/// <summary>
	/// Parses a string to the correct type or default value
	/// </summary>
	public static T ToType<T>(this string value, T @default) {
		return Parse.TryParse(value, @default);
	}
	
}

This would allow you to perform the conversions from the string itself and avoid the call to a static method floating around in the project.

bool admin = "true".ToType<bool>();
int count = "3334".ToType<int>();
Color blue = "something".ToType(Color.Blue);
Color green = "#00ff00".ToType(Color.Red);

Parsing values isn’t hard but is certainly doesn’t hurt to make it a bit easier.

Advertisements

Written by hugoware

August 5, 2010 at 1:29 am

4 Responses

Subscribe to comments with RSS.

  1. I like the idea of this, so much so.. I think i’ll use it myself as a standard in why common actions class.

    Arran

    August 5, 2010 at 2:59 pm

    • Thanks Arran, I hope it helps you out.

      If you end up writing a more robust library for this let me know and I’ll include it as a resource in the blog post.

      hugoware

      August 9, 2010 at 8:37 am

  2. It doesn’t seem to work for nullables since the “Parse” method is not found there, e.g.: Parse.TryParse(“2”) always returns null.

    greetz

    Koen

    August 13, 2010 at 12:08 pm

    • Good catch – Code has been updated (now also works for Enumerated types too!)

      hugoware

      August 13, 2010 at 12:14 pm


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: