Buyers Beware

In the build versus buy debate, which has many different considerations for software development companies, I generally lean towards the buy option. The cost to develop, support and maintain your own component code is often far greater than buying the component if available. Purchasing the code for a complete application with the intent to extend has completely different arguments and implications.

One of the first tasks was to write a new installer. The reason for the new installer was to implement it using WiX so that we can have full control over the installation and bring it in line with the rest of our application installers. I have built many installers and once I had the information I needed, this task went smoothly. I will talk more about installation creation with WiX soon.

This particular application is an Excel Add-in, written in VB.NET. One of the issues that was raised has been is the poor performance of the add-in. Doing a refresh from the data source takes almost 10 seconds. Some VBA code was written by the consultant that raised the issue to attempt to determine if the bottleneck was with the add-in or the data source. The VBA code returned the same data set almost instantaneously. Of course, the add-in performs more on a refresh than just what the VBA code did, but difference is unacceptable.

It was time to dive into the code. Running performance analysis over the application quickly pointed to the violating method. Unfortunately, the method was over 700 functional lines long! Before I could make performance improvements, I needed to understand what the code was doing. Before I could understand what the code was doing, I needed to refactor the method into manageable-sized methods. With the wide scope and reuse of variables, and the lack of unit tests, this became a problematic task. Performing a complexity analysis over the method, gave it the lowest maintainability index possible. However today, it has ended well, with the refresh now taking underĀ 2 seconds!

In following posts, I will be talking about the Installation Creation, Refactoring and Performance Improvements undertaken on this code.

Advertisements

Parse TimeSpan String

On one of my projects on the side, I had the need to for a user to enter a time estimate. The Parse method from the TimeSpan object is limited in usefulness to convert a string to a TimeSpan due to the strict requirements of the string format. From the documentation:

The s parameter contains a time interval specification of the form:

[ws][-]{ d | [d.]hh:mm[:ss[.ff]] }[ws]

Items in square brackets ([ and ]) are optional; one selection from the list of alternatives enclosed in braces ({ and }) and separated by vertical bars (|) is required; colons and periods (: and .) are literal characters and required; other items are as follows.

Item

Description

ws

optional white space

"-"

optional minus sign indicating a negative TimeSpan

d

days, ranging from 0 to 10675199

hh

hours, ranging from 0 to 23

mm

minutes, ranging from 0 to 59

ss

optional seconds, ranging from 0 to 59

ff

optional fractional seconds, consisting of 1 to 7 decimal digits

The components of s must collectively specify a time interval greater than or equal to MinValue and less than or equal to MaxValue.

This is fine, but not easy to train a user to use. What I require is a user to be able to enter an estimated time, in a simple free form way that makes sense to them. I would like automatic conversion between units, so that if the user enters 180 minutes, it is parsed to 3 hours. I would like to be able configure whether 1 day is equal to 24 hours or an 8 hour work day and configure what the default unit is, if none is specified by the user. Input should be of the format:

\s*(?<quantity>\d+)\s*(?<unit>((d(ays?)?)|(h((ours?)|(rs?))?)|(m((inutes?)|(ins?))?)|(s((econds?)|(ecs?))?)|\Z))+

Using values (removing milliseconds) from the TimeSpan Parse examples:

String to Parse

TimeSpan

0

00:00:00

1h2m3s

01:02:03

180mins

03:00:00

10 days 20 hours 30 minutes 40 seconds

10.20:30:40

99 d 23 h 59 m 59 s

99.23:59:59

23hrs59mins59secs

23:59:59

24 hours

1.00:00:00

60 min

01:00:00

60 sec

00:01:00

10

10:00:00 (if hours is default unit)

If .NET 3.5 Extension Methods supported static extension methods I would add the method public static TimeSpan ParseFreeForm(static TimeSpan timeSpan, string s) to the TimeSpan class. This would allow a TimeSpan.ParseFreeForm to be seen in the intellisense next to the inbuilt Parse method, which I think is a logical place with higher visibility than a utility class. There is obviously arguments for and against this, but I’m not going to get into that now. Since extension methods only allow new instance methods it does not make sense to create a new instance of a TimeSpan to parse a string to return a new TimeSpan. Therefore I created the utility method:

public static TimeSpan ParseTimeSpan(string s)
{
    const string Quantity = "quantity";
    const string Unit = "unit";

    const string Days = @"(d(ays?)?)";
    const string Hours = @"(h((ours?)|(rs?))?)";
    const string Minutes = @"(m((inutes?)|(ins?))?)";
    const string Seconds = @"(s((econds?)|(ecs?))?)";

    Regex timeSpanRegex = new Regex(
        string.Format(@"\s*(?<{0}>\d+)\s*(?<{1}>({2}|{3}|{4}|{5}|\Z))",
                      Quantity, Unit, Days, Hours, Minutes, Seconds), 
                      RegexOptions.IgnoreCase);
    MatchCollection matches = timeSpanRegex.Matches(s);

    TimeSpan ts = new TimeSpan();
    foreach (Match match in matches)
    {
        if (Regex.IsMatch(match.Groups[Unit].Value, @"\A" + Days))
        {
            ts = ts.Add(TimeSpan.FromDays(double.Parse(match.Groups[Quantity].Value)));
        }
        else if (Regex.IsMatch(match.Groups[Unit].Value, Hours))
        {
            ts = ts.Add(TimeSpan.FromHours(double.Parse(match.Groups[Quantity].Value)));
        }
        else if (Regex.IsMatch(match.Groups[Unit].Value, Minutes))
        {
            ts = ts.Add(TimeSpan.FromMinutes(double.Parse(match.Groups[Quantity].Value)));
        }
        else if (Regex.IsMatch(match.Groups[Unit].Value, Seconds))
        {
            ts = ts.Add(TimeSpan.FromSeconds(double.Parse(match.Groups[Quantity].Value)));
        }
        else
        {
            // Quantity given but no unit, default to Hours
            ts = ts.Add(TimeSpan.FromHours(double.Parse(match.Groups[Quantity].Value)));
        }
    }
    return ts;
}

To modify the hours in a day, when a match is made on the Day unit TimeSpan.FromHours(Quantity * hoursInDay) is all that is required. The value hoursInDay could be passed as a parameter or set as an Application Configuration value. The structure of this solution also provides the ability to easily extend for other units, such as weeks.