I'd like to make parsing small samples of data more efficient (from the developer point of view). This means, instead of writing the parsing logic each time from scratch when I encounter something as simple as Car4
or Ticket#123/22APR
It'd be great to have something that I can reuse.
So I was experimenting with dynamics and tuples. I didn't like the first one because then you need to cast every property and you lose compilation time checking. Tuples seemed like a better choice but they are not perfect (yet). They lack a feature that would make the following code much prettier which is, you cannot actually use reflection to access the property names. Because of that, I had to add a list of their names as params string[]
.
Example
The idea is to have a general purpose Parse
extension taking a regex and based on group names map matches to tuple properties. The generic arguments specify the variable types and the names after the pattern specify the order of the properties (regex groups might be in different order).
var (none, _) = "".Parse<string, object>(@"(?<Name>(?i:[a-z]+))", "name", "count");
var (name, count) = "John3".Parse<string, int?>(@"(?<Name>(?i:[a-z]+))(?<Count>\d+)?", "name", "count");
none.Dump(); // null
name.Dump(); // John
count.Dump(); // 3
Implementaion
The user API is simple. Just a couple of Parse
extensions. Internally they try to parse the input
and then call the appropriate Deconstructor
.
public static class StringExtensions
{
public static Deconstructor<T1, T2> Parse<T1, T2>(this string input, string pattern, params string[] propertyNames)
{
return new Deconstructor<T1, T2>(input.Parse(pattern), propertyNames);
}
public static Deconstructor<T1, T2, T3> Parse<T1, T2, T3>(this string input, string pattern, params string[] propertyNames)
{
return new Deconstructor<T1, T2, T3>(input.Parse(pattern), propertyNames);
}
private static IDictionary<string, string> Parse(this string input, string pattern)
{
var match = Regex.Match(input, pattern, RegexOptions.ExplicitCapture);
return
match.Success
? match
.Groups
.Cast<Group>()
// First group is the entire match. We don't need it.
.Skip(1)
.Where(g => g.Success)
.ToDictionary(
g => g.Name,
g => string.IsNullOrEmpty(g.Value) ? null : g.Value
)
: new Dictionary<string, string>();
}
}
Deconstructor
s are types that consume the dictionary created from the groups and a list of names specifying the order of the properties (they have to match with the generic types). Then they use the Deconstruct
method to create the final tuple. The first Deconstructor
also provides a method for converting strings to the target type.
public class Deconstructor<T1, T2> : Dictionary<string, string>
{
private readonly IList<string> _itemNames;
public Deconstructor(IDictionary<string, string> data, IList<string> itemNames) : base(data, StringComparer.OrdinalIgnoreCase)
{
// Shift items to the right to use indexes that are compatible with items later.
_itemNames = itemNames.Prepend(null).ToList();
}
public void Deconstruct(out T1 item1, out T2 item2)
{
Convert<T1>(1, out item1);
Convert<T2>(2, out item2);
}
protected void Convert<T>(int itemIndex, out T result)
{
if (this.TryGetValue(_itemNames[itemIndex], out var value))
{
if (value is null)
{
result = default;
}
else
{
var isNullable =
typeof(T).IsGenericType &&
typeof(T).GetGenericTypeDefinition() == typeof(Nullable<>);
var targetType =
isNullable
? typeof(T).GetGenericArguments().Single()
: typeof(T);
result = (T)System.Convert.ChangeType(value, targetType);
}
}
else
{
result = default;
}
}
}
Each other Deconstructor
s is based on the one that is taking one generic parameter less.
public class Deconstructor<T1, T2, T3> : Deconstructor<T1, T2>
{
public Deconstructor(IDictionary<string, string> data, IList<string> names) : base(data, names) { }
public void Deconstruct(out T1 item1, out T2 item2, out T3 item3)
{
base.Deconstruct(out item1, out item2);
Convert<T3>(3, out item3);
}
}
This prototype works quite well but perhaps it can still be made better. What do you think?