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)?
2 Answers 2
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.
-
\$\begingroup\$ Thanks, unfortunately I'm developing for Qt / QML and the JavaScript engine over there does not support
matchAll()
. \$\endgroup\$Delgan– Delgan2020年05月11日 11:12:22 +00:00Commented 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\$CertainPerformance– CertainPerformance2020年05月11日 11:16:36 +00:00Commented May 11, 2020 at 11:16
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))
5-3
too, that provides[ 5, 4, 3 ]
\$\endgroup\$.split("-").map(Number)
and then usingMath.min()
/Math.max()
for iteration. \$\endgroup\$