I am developing an app that takes track of transcurred times, and the user must be able to set the app time format as he wants, for example:
"ss:fff" (seconds:miliseconds)
"ss\s\. fff mils\." (seconds s. miliseconds mils.)
"dd:hh:mm" (days:hours:minutes)
etc...
I store the times as long, so with a simple TimeSpan formatting I can show them with the user configurated format, easy and actually working.
The problem came up when I started to implement a "by-hand" time addition (the user types a new time in a TextBox and it's added to the times list with the configured time format).
Just after the user introduced a new time I have to convert the introduced time from string to long with the prupose of storing it (TimeSpan.TryParseExact providing the configured time format does the work), except for one problem: if we have a format like mm:ss and the parsed time is something as 90:32, the parse fails because the time to parse should not have > 59 minutes.
I made a small Console App example to help reproduce my issue:
static void Main(string[] args)
{
string TimeFormat = @"ss\:fff";
long[] SampleTimes = new long[] { 1000, 5000, 59666 };
List<long> times = new List<long>(SampleTimes);
string input;
long aux;
do
{
ShowTimes(times, TimeFormat);
Console.Write(">");
input = Console.ReadLine();
if (TryParseTime(input, TimeFormat, out aux))
times.Add(aux);
else
Console.WriteLine("Failed parsing");
} while (input != "Exit");
}
static void ShowTimes(IEnumerable<long> times, string format)
{
Console.WriteLine("-----");
foreach (long time in times)
Console.WriteLine(TimeSpan.FromMilliseconds(time).ToString(format));
Console.WriteLine("-----");
}
static bool TryParseTime(string time, string format, out long parsed)
{
TimeSpan result;
bool ok = TimeSpan.TryParseExact(time, format, null, out result);
parsed = ok ? (long)result.TotalMilliseconds : -1;
return ok;
}
In another posts [1, 2] referencing the same issue they worked around it separating the parts of the introduced time and calculating it from code:
//From first post
var temp = "113388";
s_Time = DateTime.ParseExact(temp.Substring(0, 4
), "HHmm", null).AddSeconds(int.Parse(temp.Substring(4)));
//From second post
public static decimal Hours(string s)
{
decimal r;
if (decimal.TryParse(s, out r))
return r;
var parts = s.Split(':');
return (decimal)new TimeSpan(int.Parse(parts[0]), int.Parse(parts[1]),0).TotalHours;
}
But I cannot folow this way because there is not a time format to take as reference to split the introduced one, it can change at any moment.
The only idea I got at this moment is to create a TimeSpan.TryParseExact expansion method that takes the biggest time unit by regex and parses it by separate...
Any better way to do that?
Ok guys, I ended up with this custom method to do the work.
It's not a method to execute many times in a row because of the huge performance issues it will have, but to parse the introduced data from the front end it's more than acceptable:
/// <summary>
/// Given a time and a format it creates a <see cref="TimeSpan"/> ignoring the format digit limitations.
/// The format is not validated, so better ensure a correct one is provided ;)
/// </summary>
/// <param name="time"></param>
/// <param name="format"></param>
/// <param name="timeSpan"></param>
/// <returns></returns>
public static bool TryParseTime(string time, string format, out TimeSpan timeSpan)
{
// Regex to match the components of the time format (ss:fff matches ss and fff)
var formatRegex = new Regex(@"(?<=(?<!\\)(?:\\{2})*)(%?([fFsmhd])(\2*))");
var matches = formatRegex.Matches(format);
if (matches.Count > 0)
{
// We build a big regex to extract the values from time
string formatExtractionRegex = string.Empty;
int pivot = 0;
foreach (Match match in matches)
{
if (match.Success)
{
char c = match.Value.ToLower()[0];
formatExtractionRegex += $@"{format.Substring(pivot, match.Index - pivot)}(?<{c}>\d+)";
pivot = match.Index + match.Length;
}
}
var timeParts = new Regex(formatExtractionRegex).Match(time);
int d, h, m, s, f;
int.TryParse(timeParts.Groups["d"].ToString(), out d);
int.TryParse(timeParts.Groups["h"].ToString(), out h);
int.TryParse(timeParts.Groups["m"].ToString(), out m);
int.TryParse(timeParts.Groups["s"].ToString(), out s);
int.TryParse(timeParts.Groups["f"].ToString(), out f);
timeSpan = new TimeSpan(d, h, m, s, f);
return true;
}
timeSpan = default;
return false;
}
The method extracts the data from the time by building a big regex that replaces the digit type for the regex expression \d+
, so we select entire digit groups when they are longer than what the format specifies.
If we provide a time 100:100:5000
and a format mm\:ss\:fff
, the generated regex will be (?<m>\\d+)\\:(?<s>\\d+)\\:(?<f>\\d+)
.
Finally we parse the matched groups and we parse them to be given to the TimeSpan Constructor.