A prettier way to parse strings in C#

A prettier way to parse strings in C#

Friday 11 January 2013

Parsing strings in C# can be verbose and horrible.

The last thing you want to do when solving some real problem, is end up with a load of code extracting values from strings and marshalling them between data types. It's silly, so lets not do it!

Instead, I offer you an extension method and some tests. Do with them what you will!

	
public class StringParsingNeedsToBeSimpler
{
    public void HereAreSomeExamples()
    {
        //You know what sucks?
        string value = "123";
        int outt;
        if(!int.TryParse(value, out outt))
        {
            outt = 12345; // my default!
        }
    // That's ugly and verbose.
    // So lets try these instead!

    var betterWay = value.Parse(onFailure: () => 12345);
    var evenBetter = value.Parse(() => 12345);
    var youCanEvenDo = value.Parse<double>();

    var andCheckThisOut = "01/01/2012".Parse<DateTime>();
    var andThis = "01/01/2012".Parse<DateTime>(()=>DateTime.Now);

    // Now isn't that a nice concise way to parse!
    // Tests included below.
}

}

	

using System;

public static class TypeParsingExtensions
{
    public static object Parse(this string source, Type ttype, Func<object> onFailure = null)
    {
        object parsed;

        try
        {
            parsed = Convert.ChangeType(source, ttype);
        }
        catch (Exception)
        {
            if (onFailure == null)
            {
                throw;
            }

            return onFailure();
        }

        return parsed;
    }

    public static TType Parse<TType>(this string source, Func<TType> onFailure = null)
    {
        return (TType)source.Parse(typeof(TType), onFailure != null ? (Func<object>)(() => onFailure()) : null);
    }
}

	
	

using System;
using System.Collections.Generic;
using NUnit.Framework;

[TestFixture]
public class TypeParsingExtensionsTests
{
    private readonly List<TestCase> _testCases;

    public TypeParsingExtensionsTests()
    {
        _testCases = new List<TestCase>
                {
                    TestCase.For(validExample: 123, @default: 987),
                    TestCase.For(validExample: 123.54m, @default: 987.25m),
                    TestCase.For<float>(validExample: 123456800, @default: 987654321),
                    TestCase.For<byte>(validExample: 1, @default: 2),
                    TestCase.For(validExample: 123.456, @default: 543.321),
                };
    }

    // Tests use Parse(type) rather than Parse>type<() to keep them DRY (if slightly less clear).

    [Test]
    public void Parse_PassedValid_Parses()
    {
        foreach (var testCase in _testCases)
        {
            var output = testCase.Valid.Parse(testCase.TestType);
            Assert.That(output, Is.EqualTo(testCase.Expectation));
        }
    }

    [Test]
    public void Parse_PassedInvalid_ThrowsFormatException()
    {
        foreach (var testCase in _testCases)
        {
            Assert.Throws>formatexception<(() => testCase.Invalid.Parse(testCase.TestType));
        }
    }

    [Test]
    public void Parse_PassedInvalidWithDefault_ReturnsDefault()
    {
        foreach (var testCase in _testCases)
        {
            var @case = testCase;
            var output = testCase.Invalid.Parse(testCase.TestType, () => @case.Default);
            Assert.That(output, Is.EqualTo(testCase.Default));
        }
    }

    private class TestCase
    {
        public string Valid { get; private set; }
        public string Invalid { get; private set; }
        public object Expectation { get; private set; }
        public object Default { get; private set; }
        public Type TestType { get; private set; }

        public static TestCase For<TType>(TType validExample, TType @default)
        {
            return new TestCase(validExample.ToString()) { Expectation = validExample, Default = @default, TestType = typeof(TType) };
        }

        private TestCase(string valid)
        {
            Valid = valid;
            Invalid = "Invalid";
        }
    }
}
	

You’ll either end up with the parsed string, your default if you provide one and parsing fails, or whatever exception would have occurred naturally if you don’t. I think I’ve written something similar to this in everything I’ve ever worked on at some point or another, so I’ll leave it here for reference.

Some sweet sweet syntactic sugar. (duplicated on: https://gist.github.com/4512395)