Marian
Marian

Reputation: 191

Regex: Capture multiple optional groups with a single match

In JavaScript, I'm looking for a regex to catch multiple optional groups in a string. But at least one of the groups should exist.

String: foo bar 12 seconds 3minutes 4h

Regex so far: /(?:(\d+)\s?s(?:econds?)?)?(?:(\d+)\s?m(?:inutes?)?)?(?:(\d+)\s?h(?:ours?)?)?/gi

I need to capture 12 seconds 3minutes and 4h, returning only the number values in their respective groups.

These time units can either exist or be swapped around. My final result would need to look like this:

12s 3m //['12', '3', undefined]
10 seconds //['10', undefined, undefined]
4hours //[undefined, undefined, '4']
3 minutes //[undefined, '3', undefined]
1hour 54seconds 7minutes //['54', '7', '1']

undefined null or even an empty string. As long as they are in their respective index.

Any easy way to handle this with a single exec or match without using loops?

Upvotes: 0

Views: 619

Answers (3)

ridgerunner
ridgerunner

Reputation: 34385

As Wiktor correctly states, there is no way to do this with a single regex. Here is a simple function that implements a 3-regex solution:

function get_time_parts(text) {
    var s, m, h;
    // Seconds part: Either  "s", "sec", "secs" "second" or "seconds".
    s = text.match(/\b(\d+)\s*s(?:ec(?:ond)?s?)?\b/i);
    s = s ? s[1] : undefined;
    // Minutes part: Either "m", "min", "mins" "minute" or "minutes".
    m = text.match(/\b(\d+)\s*m(?:in(?:ute)?s?)?\b/i);
    m = m ? m[1] : undefined;
    // Hours part: Either "h", "hr", "hrs" "hour" or "hours".
    h = text.match(/\b(\d+)\s*h(?:rs?|ours?)?\b/i);
    h = h ? h[1] : undefined;
    return (s || m || h) ? [s, m, h] : null;
}

As stated in the comments, this function allows the following time part variations:

Seconds part: Either "s", "sec", "secs" "second" or "seconds".
Minutes part: Either "m", "min", "mins" "minute" or "minutes".
Hours part: Either "h", "hr", "hrs" "hour" or "hours".

The regexes are case insensitive so will allow variations, e.g. HR, Sec, mIN, etc. If none of the parts are present, the function returns null.

Upvotes: 1

Kevin B.
Kevin B.

Reputation: 1363

There is no simple solution to do this with plain regex. The simplest solution is to use the exec method and to set the values to hash (object). Furthermore you can simplify your regex - All inute, econd, our are completely useless in your regex. If you want only s or second you should use (?:s|second), because in your example 5 samples would match too.

The easiest solution for your problem (without handling the order of the units):

var str  = "foo bar 12 seconds 5m 4hours";
var re = /(\d+)\s*([smh])/gi
var hash = {};

var m;
while ((m = re.exec(str)) !== null) {
  // get values
  var value = m[1];
  var unit = m[2].toLowerCase();

  // set value
  hash[unit] = value;
}

console.log(hash);

This solution will always use the last occur and will not depend on the order of the units.

Upvotes: 1

JBone
JBone

Reputation: 1794

Not sure this matches to you various types of input strings, but here is something that I came up with for the input string you put up there. I assumed the seconds come first, minutes next and then hours as you have it in your question input string. Is this order correct all the time?

let str = "foo bar 12 seconds 3minutes 4h";
let result = str.match(/(\d+) ?(?:sec|seconds) ?(\d+) ?(?:min|minutes) ?(\d+) ?(?:h|hours?)/);
console.log(`${result[3]}hour ${result[1]}second ${result[2]}minutes`);  

Upvotes: -1

Related Questions