2
\$\begingroup\$

In my library for styling the Console I need to be able to add or update two styles: color and background-color.

The use case is very simple. There is a string with styles that I'm building and that I update to change the color if necessary.

I divided this task into three steps:

  1. explode the string into declarations
  2. manipulate the style
  3. reconstruct the style

Example Each of these steps has its own extension:

void Main()
{
    var style = "color: red;";

    style
        .ToDeclarations()
        .AddOrUpdate(
            ("color", "black"),
            ("background-color", "blue")
        )
        .ToStyle()
        .Dump();
}

These extensions are used by higher-level extensions that call them with const string for property names. I just used simple strings in LINQPad.


Code Their implementation is very straightforward. They use only LINQ and tuples.

public static class CssExtensions
{
    public static IEnumerable<(string property, string value)> ToDeclarations(
        this string style)
    {
        const string declarationPattern = @"(?<property>[a-z-]+):\s*(?<value>.+?)(;|$)";

        return
            Regex
                .Matches(style, declarationPattern, RegexOptions.IgnoreCase)
                .Cast<Match>()
                .Select(m => (
                    property: m.Value("property").Trim(),
                    value: m.Value("value").Trim())
                );
    }

    public static IEnumerable<(string property, string value)> AddOrUpdate(
        this IEnumerable<(string property, string value)> declarations,
        params (string property, string value)[] updates)
    {
        return
            declarations
                .Concat(updates)
                .GroupBy(t => t.property)
                .Select(t => t.Last())
                .Skip(t => string.IsNullOrEmpty(t.value));
    }

    public static string ToStyle(
        this IEnumerable<(string property, string value)> declarations)
    {
        return
            declarations
                .Select(t => $"{t.property}: {t.value};")
                .Join(" ");
    }
}

As the use case is very simple and I also don't need to parse any complex styles so are the extensions, just a simple regex with some string manipulation.

I update the style by grouping by the property and pick the last one if there are mutliple ones, if there is none yet, then a new style is added. If the value happens to be null or Empty then it's removed.


There are three more extensions: one that I use to Join declarations, one Value shortcut for regex and a Skip that I use as an inverse of Where to use positive conditions if I want to exclude some cases.

public static class EnumerableExtensions
{   
    public static string Join<T>(this IEnumerable<T> values, string separator)
    {
        return string.Join(separator, values);
    }

    public static IEnumerable<T> Skip<T>(this IEnumerable<T> source, Func<T, bool> predicate)
    {
        return source.Where(x => !predicate(x));
    }
}

public static class MatchExtensions
{
    public static string Value(this Match match, string groupName)
    {
        return match.Groups[groupName].Value;
    }
}

Should I still improve it in any way?

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

The only thing that pops out to me (though admittedly, it is very minor) is that you're recompiling the regex every time you call ToDeclarations.

You could use a static Regex instance which compiles the expression only once. Regex instances are also threadsafe - so no worries there.

Thus, your code could look like:

private static Regex _cssRegex = new Regex("(?<property>[a-z-]+):\s*(?<value>.+?)(;|$)", RegexOptions.IgnoreCase);
public static IEnumerable<(string property, string value)> ToDeclarations(this string style)
{
    return
        _cssRegex
            .Matches(style)
            .Cast<Match>()
            .Select(m => (
                property: m.Value("property").Trim(),
                value: m.Value("value").Trim())
            );
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.