Add parser for simple range expression

Implement parsing of ranges of the form "1, 3-5" into a set of all the
numbers it represents.
This commit is contained in:
Hornwitser committed 2024-01-11 00:19:19 +01:00
1 parent c87493af0c
commit 9742eb1645
2 files changed
+160 -4

No files matched your search

+107 -4
View File
@@ -230,6 +230,14 @@ export function formatBytes(bytes: number) {
return (power > 0 ? (bytes / factor ** power).toFixed(2) : bytes) + units[power];
}
function skipWhitespace(pos: number, input: string) {
// whitespace = 1*" "
while (pos < input.length && input.charAt(pos) === " ") {
pos += 1;
}
return pos;
}
function parseSearchIdentifier(pos: number, input: string): [number, string] {
// character = ? all characters excluding space : " ?
// identifier = 1*character
@@ -393,7 +401,6 @@ export function parseSearchString(
input: string,
attributes: Record<string, string> = {}
): ParsedSearch {
// whitespace = 1*" "
// search = [ term, *( [ whitespace ], term ) ]
input = input.trim();
let parsed = {
@@ -407,9 +414,7 @@ export function parseSearchString(
if (term) {
parsed.terms.push(term);
}
while (pos < input.length && input.charAt(pos) === " ") {
pos += 1;
}
pos = skipWhitespace(pos, input);
}
// istanbul ignore if (should not be possible)
if (pos !== input.length) {
@@ -417,3 +422,101 @@ export function parseSearchString(
}
return parsed;
}
function isDigit(pos: number, input: string) {
const code = input.charCodeAt(pos);
return "0".charCodeAt(0) <= code && code <= "9".charCodeAt(0);
}
function parseNumber(pos: number, input: string): [number, number] {
// number = 1*('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9')
let startPos = pos;
let endPos = pos;
while (pos < input.length && isDigit(pos, input)) {
pos += 1;
endPos += 1;
}
return [pos, Number.parseInt(input.slice(startPos, endPos), 10)];
}
function parseRange(pos: number, input: string, min: number, max: number): [number, Set<number>] {
// range = number, [ whitespace ], [ '-', [ whitespace ], number ]
const range = new Set<number>();
if (!isDigit(pos, input)) {
throw new Error(`Expected digit but got '${input[pos]}' at pos ${pos} while parsing "${input}"`);
}
let start: number;
([pos, start] = parseNumber(pos, input));
pos = skipWhitespace(pos, input);
if (pos < input.length && input[pos] === "-") {
pos += 1; // Skip dash
pos = skipWhitespace(pos, input);
if (pos === input.length) {
throw new Error(`Expected digit but got end of input while parsing "${input}"`);
}
if (!isDigit(pos, input)) {
throw new Error(`Expected digit but got '${input[pos]}' at pos ${pos} while parsing "${input}"`);
}
let end: number;
([pos, end] = parseNumber(pos, input));
if (start > end) {
([start, end] = [end, start]);
}
if (start < min) {
throw new Error(`start of range ${start}-${end} is below the minimum value ${min}`);
}
if (end > max) {
throw new Error(`end of range ${start}-${end} is above the maximum value ${max}`);
}
for (let i = start; i <= end; i++) {
range.add(i);
}
} else { // Single value range
if (start < min) {
throw new Error(`value ${start} is below the minimum value ${min}`);
}
if (start > max) {
throw new Error(`value ${start} is above the maximum value ${max}`);
}
range.add(start);
}
return [pos, range];
}
/**
* Parse a comma separated range expression
*
* Parses the given input as a series of comma sepparated number ranges
* where each range can consist of either a whole number or a whole number
* of where the range starts, a dash and then a whole number of where the
* range ends inclusively.
*
* @param input - Range expression to parse
* @param min - Minimum accepted input value.
* @param max - Maximum accepted input value.
* @returns Set of all numbers in the parsed range.
*/
export function parseRanges(
input: string,
min: number,
max: number,
) {
// ranges = [ range, *( [ whitespace ], [ ',', [ whitespace ] ], range ) ]
input = input.trim();
const parsed = new Set<number>();
let pos = 0;
while (pos < input.length) {
let range: Set<number>;
([pos, range] = parseRange(pos, input, min, max));
for (const i of range) {
parsed.add(i);
}
pos = skipWhitespace(pos, input);
if (pos < input.length && input[pos] === ",") {
pos += 1;
pos = skipWhitespace(pos, input);
}
}
return parsed;
}
+53
View File
@@ -300,4 +300,57 @@ describe("lib/helpers", function() {
);
});
});
describe("parseRanges()", function() {
function range(input, min=1, max=10) {
return [...lib.parseRanges(input, min, max)];
}
function expectedRange(start, end) {
const items = new Set();
for (let i = start; i <= end; i++) {
items.add(i);
}
return [...items];
}
it("should parse numbers separated by spaces and or commas", function() {
assert.deepEqual(range("1"), [1]);
assert.deepEqual(range(" 1"), [1]);
assert.deepEqual(range("1 "), [1]);
assert.deepEqual(range(" 1 "), [1]);
assert.deepEqual(range("1 2"), [1, 2]);
assert.deepEqual(range("1, 2"), [1, 2]);
assert.deepEqual(range("1 , 2"), [1, 2]);
assert.deepEqual(range(" 1 2 "), [1, 2]);
});
it("should parse ranges", function() {
assert.deepEqual(range("1-4"), [1, 2, 3, 4]);
assert.deepEqual(range(" 1-4 "), [1, 2, 3, 4]);
assert.deepEqual(range(" 1 - 4 "), [1, 2, 3, 4]);
assert.deepEqual(range("1- 4"), [1, 2, 3, 4]);
assert.deepEqual(range("1 -4"), [1, 2, 3, 4]);
assert.deepEqual(range(" 1, 2-4"), [1, 2, 3, 4]);
assert.deepEqual(range("4-1"), [1, 2, 3, 4]);
});
it("should ignore duplicates in ranges", function() {
assert.deepEqual(range("1, 2, 2, 2"), [1, 2]);
assert.deepEqual(range("1-4, 3, 4"), [1, 2, 3, 4]);
assert.deepEqual(range("3, 1-4"), [3, 1, 2, 4]);
});
it("should throw on bad input", function() {
assert.throws(() => range("1-"), new Error('Expected digit but got end of input while parsing "1-"'));
assert.throws(() => range("a1-4"), new Error('Expected digit but got \'a\' at pos 0 while parsing "a1-4"'));
assert.throws(() => range("1a-4"), new Error('Expected digit but got \'a\' at pos 1 while parsing "1a-4"'));
assert.throws(() => range("1-a4"), new Error('Expected digit but got \'a\' at pos 2 while parsing "1-a4"'));
assert.throws(() => range("1-4a"), new Error('Expected digit but got \'a\' at pos 3 while parsing "1-4a"'));
});
it("should throw if out of range", function() {
assert.throws(() => range("0"), new Error("value 0 is below the minimum value 1"));
assert.throws(() => range("110"), new Error("value 110 is above the maximum value 10"));
assert.throws(() => range("1-999"), new Error("end of range 1-999 is above the maximum value 10"));
assert.throws(() => range("0-1"), new Error("start of range 0-1 is below the minimum value 1"));
});
it("should handle 16-bit ranges", function() {
assert.deepEqual(range("1-65535", 1, 2**16 - 1), expectedRange(1, 2**16 - 1));
assert.deepEqual(range("20000-20002", 1, 2**16 - 1), [20000, 20001, 20002]);
});
});
});