Extract minutes from time string in "jira notation"
Asked Answered
L

5

5

I am trying to extract the number of minutes from user input that is entered in "jira time notation".

For example, I would like to achieve the following results

  • Input: "30m" / Output: 30
  • Input: "1h 20m" / Output: 80
  • Input: "3h" / Output 180

From research, I have found TimeSpan.ParseExact but I can't figure out how to use this to achieve what I need.

All help would be very much appreciated.

My code so far:

public static int TextToMins(string TimeText)
    {
        CultureInfo culture = new CultureInfo("en-IE");
        string[] formats = { "What goes here?" };
        TimeSpan ts = TimeSpan.ParseExact(TimeText.Trim().ToLower(), formats, culture);
        return Convert.ToInt32(ts.TotalMinutes);
    }
Lawn answered 22/10, 2013 at 21:2 Comment(1)
I have edited your title. Please see, "Should questions include “tags” in their titles?", where the consensus is "no, they should not".Mara
M
3

I'd probably do something like this and avoid mucking about with Timespan's built-in parsing, given that days per [work] week and [working] hours per [work] day are configurable values in Jira (On our system they are configured as 5 days per week and 8 hours per day, respectively.)

class JiraTimeDurationParser
{

  /// <summary>
  /// Jira's configured value for [working] days per week ;
  /// </summary>
  public ushort DaysPerWeek     { get ; private set ; }

  /// <summary>
  /// Jira's configured value for [working] hours per day
  /// </summary>
  public ushort HoursPerDay     { get ; private set ; }

  public JiraTimeDurationParser( ushort daysPerWeek = 5 , ushort hoursPerDay = 8 )
  {
    if ( daysPerWeek < 1 || daysPerWeek >  7 ) throw new ArgumentOutOfRangeException( "daysPerWeek"  ) ;
    if ( hoursPerDay < 1 || hoursPerDay > 24 ) throw new ArgumentOutOfRangeException( "hoursPerDay"  ) ;

    this.DaysPerWeek = daysPerWeek ;
    this.HoursPerDay = hoursPerDay ;

    return ;
  }

  private static Regex rxDuration = new Regex( @"
    ^                                   # drop anchor at start-of-line
      [\x20\t]* ((?<weeks>   \d+ ) w )? # Optional whitespace, followed by an optional number of weeks
      [\x20\t]* ((?<days>    \d+ ) d )? # Optional whitesapce, followed by an optional number of days
      [\x20\t]* ((?<hours>   \d+ ) h )? # Optional whitespace, followed by an optional number of hours
      [\x20\t]* ((?<minutes> \d+ ) m )  # Optional whitespace, followed by a mandatory number of minutes
      [\x20\t]*                         # Optional trailing whitespace
    $                                   # followed by end-of-line
    " ,
    RegexOptions.IgnorePatternWhitespace
    ) ;

  public TimeSpan Parse( string jiraDuration )
  {
    if ( string.IsNullOrEmpty( jiraDuration ) ) throw new ArgumentOutOfRangeException("jiraDuration");

    Match m = rxDuration.Match( jiraDuration ) ;
    if ( !m.Success ) throw new ArgumentOutOfRangeException("jiraDuration") ;

    int weeks   ; bool hasWeeks   = int.TryParse( m.Groups[ "weeks"   ].Value , out weeks   ) ;
    int days    ; bool hasDays    = int.TryParse( m.Groups[ "days"    ].Value , out days    ) ;
    int hours   ; bool hasHours   = int.TryParse( m.Groups[ "hours"   ].Value , out hours   ) ;
    int minutes ; bool hasMinutes = int.TryParse( m.Groups[ "minutes" ].Value , out minutes ) ;

    bool isValid = hasWeeks|hasDays|hasHours|hasMinutes ;
    if ( !isValid ) throw new ArgumentOutOfRangeException("jiraDuration") ;

    TimeSpan duration = new TimeSpan( weeks*DaysPerWeek*HoursPerDay + days*HoursPerDay + hours , minutes , 0 );
    return duration ;

  }

  public bool TryParse( string jiraDuration , out TimeSpan timeSpan )
  {
    bool success ;
    try
    {
      timeSpan = Parse(jiraDuration) ;
      success = true ;
    }
    catch
    {
      timeSpan = default(TimeSpan) ;
      success  = false ;
    }
    return success ;
  }

}
Marijo answered 22/10, 2013 at 21:54 Comment(0)
F
2

Bit of a sledgehammer approach, but how about:

public static int TextToMins(string timeText)
{
    var total = 0;
    foreach (var part in timeText.Split(' '))
    {
        if (part[part.Length - 1] == 'h')
        {
            total += 60 * int.Parse(part.Trim('h'));
        }
        else
        {
            total += int.Parse(part.Trim('m'));
        }
    }
    return total;
}
Fir answered 22/10, 2013 at 21:15 Comment(1)
@Lawn Glad to hear it. It's not as neat a solution to the problem as you were trying for though. But if it does the job, and is readable, it's probably good enough...Fir
B
2

The answer to "what goes here" is, a string built out of the options from Custom Timespan Format Strings. If I'm reading the documentation correctly, characters not in that list -- including whitespace -- have to be escaped with the \ character or surrounded with single quotation marks.

For example, try m\m to parse "1m", and h\h m\m to parse "1h 10m". So your code would be:

string[] formats = { "m\m", "h\h\ m\m" }; 

Caveat: I haven't tried parsing TimeSpan objects. But I have done DateTime objects, and it is very similar. So I think this should work.

Bynum answered 22/10, 2013 at 21:27 Comment(1)
This is the cleanest solution. I tested it and works. TimeSpan time = TimeSpan.ParseExact("1h 20m", "h\\h\\ m\\m", CultureInfo.InvariantCulture);Refraction
I
2

The following code will parse strings like: "1h", "1h30m", "12h 45m", "1 h 4 m", "1d 12h 34m 20s", "80h", "3000ms", "20mins", "1min".

In every case "spaces are ignored", it support "days, hours, minutes, seconds and milliseconds" but easily you can add months, weeks, years, etc... just add the right expression in the condition list.

public static TimeSpan ParseHuman(string dateTime)
{
    TimeSpan ts = TimeSpan.Zero;
    string currentString = ""; string currentNumber = "";
    foreach (char ch in dateTime+' ')
        {
            currentString += ch;
            if (Regex.IsMatch(currentString, @"^(days(\d|\s)|day(\d|\s)|d(\d|\s))", RegexOptions.IgnoreCase)) { ts = ts.Add(TimeSpan.FromDays(int.Parse(currentNumber))); currentString = ""; currentNumber = ""; }
            if (Regex.IsMatch(currentString, @"^(hours(\d|\s)|hour(\d|\s)|h(\d|\s))", RegexOptions.IgnoreCase)) { ts = ts.Add(TimeSpan.FromHours(int.Parse(currentNumber))); currentString = ""; currentNumber = ""; }
            if (Regex.IsMatch(currentString, @"^(ms(\d|\s))", RegexOptions.IgnoreCase)) { ts = ts.Add(TimeSpan.FromMilliseconds(int.Parse(currentNumber))); currentString = ""; currentNumber = ""; }
            if (Regex.IsMatch(currentString, @"^(mins(\d|\s)|min(\d|\s)|m(\d|\s))", RegexOptions.IgnoreCase)) { ts = ts.Add(TimeSpan.FromMinutes(int.Parse(currentNumber))); currentString = ""; currentNumber = ""; }
            if (Regex.IsMatch(currentString, @"^(secs(\d|\s)|sec(\d|\s)|s(\d|\s))", RegexOptions.IgnoreCase)) { ts = ts.Add(TimeSpan.FromSeconds(int.Parse(currentNumber))); currentString = ""; currentNumber = ""; }
            if (Regex.IsMatch(ch.ToString(), @"\d")) { currentNumber += ch; currentString = ""; }
        }
    return ts;
}
Inaptitude answered 6/4, 2017 at 15:46 Comment(2)
I liked the idea. But it fails for string 3days 20hours 36mins 17secs 156msMeson
@Milad, You are right, I fixed it, now it covers all cases.Inaptitude
C
1

Just had the same problem. Here's my solution (unit tested), for what it's worth:

public static TimeSpan Parse(string s)
{
    long seconds = 0;
    long current = 0;

    int len = s.Length;
    for (int i=0; i<len; ++i)
    {
        char c = s[i];

        if (char.IsDigit(c))
        {
            current = current * 10 + (int)char.GetNumericValue(c);
        }
        else if (char.IsWhiteSpace(c))
        {
            continue;
        }
        else
        {
            long multiplier;

            switch (c)
            {
                case 's': multiplier = 1; break;      // seconds
                case 'm': multiplier = 60; break;     // minutes
                case 'h': multiplier = 3600; break;   // hours
                case 'd': multiplier = 86400; break;  // days
                case 'w': multiplier = 604800; break; // weeks
                default:
                    throw new FormatException(
                        String.Format(
                            "'{0}': Invalid duration character {1} at position {2}. Supported characters are s,m,h,d, and w", s, c, i));
            }

            seconds += current * multiplier;
            current = 0;
        }
    }

    if (current != 0)
    {
        throw new FormatException(
            String.Format("'{0}': missing duration specifier in the end of the string. Supported characters are s,m,h,d, and w", s));
    }

    return TimeSpan.FromSeconds(seconds);
}
Coaming answered 4/2, 2014 at 16:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.