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.