5
\$\begingroup\$

I would like to convert a string like "1 2 3-5" to an array of numbers [1, 2, 3, 4, 5].

Following the robustness principle, the list doesn't need to be separated by spaces, comma on any other separator is accepted. The only mandatory syntax is A-B to represent range from A to B (inclusive). Also, it's ok to ignore malformed input (like 1-3-5), it can be considered "undefined behavior" as long as it doesn't raise an exception.

My current code is a follow:

function parse(string) {
    let numbers = [];
    for (let match of string.match(/[0-9]+(?:\-[0-9]+)?/g)) {
        if (match.includes("-")) {
            let [begin, end] = match.split("-");
            for (let num = parseInt(begin); num <= parseInt(end); num++) {
                numbers.push(num);
            }
        } else {
            numbers.push(parseInt(match));
        }
    }
    return numbers;
}

This seems quite convoluted for such a simple task. Is there any alternative more straightforward using "modern" Javascript (I'm targeting ES6)?

\$\endgroup\$
2
  • 1
    \$\begingroup\$ I would take in account 5-3 too, that provides [ 5, 4, 3 ] \$\endgroup\$
    – Cid
    May 11, 2020 at 10:56
  • \$\begingroup\$ @Cid Yes, that's a good idea, thank you. I improved my code by calling .split("-").map(Number) and then using Math.min() / Math.max() for iteration. \$\endgroup\$
    – Delgan
    May 11, 2020 at 11:11

2 Answers 2

7
\$\begingroup\$

On modern environments that support it (which is pretty much all of them except Safari), you can make it a bit nicer by using matchAll instead of match, and use capture groups to capture the digits immediately, rather than having to use an .includes check and split afterwards:

function parse(string) {
    const numbers = [];
    for (const [, beginStr, endStr] of string.matchAll(/(\d+)(?:-(\d+))?/g)) {
        const [begin, end] = [beginStr, endStr].map(Number);
        numbers.push(begin);
        if (endStr !== undefined) {
            for (let num = begin + 1; num <= end; num++) {
              numbers.push(num);
          }
        }
    }
    return numbers;
}

console.log(parse('11 14-16 18-20'));

(For older environments, you'll need a polyfill)

Also note that you should always use const to declare variables whenever possible - only use let when you need to warn readers of the code that you may have to reassign the variable in the future.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks, unfortunately I'm developing for Qt / QML and the JavaScript engine over there does not support matchAll(). \$\endgroup\$
    – Delgan
    May 11, 2020 at 11:12
  • \$\begingroup\$ That's what polyfills are for - they add functionality to older non-standards-compliant environments that don't support modern methods. (In a reasonably professional project, I'd expect to see polyfills regardless). Without support and without the polyfill, you can do the same sort of thing with let match; while (match = pattern.exec(str)) { const [, beginStr, endStr] = match;, but that's kind of ugly. \$\endgroup\$ May 11, 2020 at 11:16
2
\$\begingroup\$

While your approach is working, it is also convoluted, which means that is hard to work with, for others, and for you in the future.

You should break it down in smaller functions, that are easier to understand, develop, test and debug.

// converts "1 2 3, 5-7" into ["1", "2", "3" "5-7"]
const sanitizeInput = input => ...

// converst "5-7" into [5, 6, 7]
const handleRange = input => ...

// return true if the string contains a dash
const isRange = input => ...

// turns [1, 2, 3, [5, 6, 7]] into [1, 2, 3, 5, 6, 7]
const flatten = input => ...

const mapValue = input => isRange(input) ? handleRange(input) : input

const processSanitizedInput = input => 
    flatten(sanitizeInput(input).map(mapValue))
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.