Search code examples
c#.nettimespandatetime-parsing

Any workaround to TimeSpan.ParseExact with more than 59 seconds?


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?


Solution

  • 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.