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.

Advertisements

4 thoughts on “Parse TimeSpan String

  1. how to test a string using your format ?I call timeSpanRegex .IsMatch("3d5hffff"),it\’s return is true,but obviously with error.

    • If you run that through the complete function it has 2 matches, hence the return true, and should result in 3 days and 5 hours.

      ParseTimeSpan(“3d5hffff”).ToString();
      Result: 3.05:00:00

  2. Nice post, has an error though.
    Just want to ask, what kind of property editor did You use that allowed You to set this RegEx as a Edit mask ?

    • I did not use an edit mask. I was only interested in extracting the useful TimeSpan. In ASP.NET you could use a regex validator to check it on the client. In WinForms it is easy enough to just validate on the text box event.

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