Commit b58ceba9 authored by Robert Knight's avatar Robert Knight

Add `pageRangesOverlap` utility

This will be used for testing whether an EPUB chapter's page range ovelaps a
page range focus filter.
parent 00531596
/**
* Parse a page number or range into a `[start, end]` integer pair. The `start`
* or `end` may be `null` if the range is open.
*
* Returns `null` if the page range could not be parsed.
*/
function parseRange(range: string): [number | null, number | null] | null {
let start;
let end;
if (range.includes('-')) {
[start, end] = range.split('-');
} else {
start = range;
end = range;
}
let startInt = null;
if (start) {
startInt = parseInt(start);
if (!Number.isInteger(startInt)) {
return null;
}
}
let endInt = null;
if (end) {
endInt = parseInt(end);
if (!Number.isInteger(endInt)) {
return null;
}
}
if (startInt === null || endInt === null) {
return [startInt, endInt];
}
if (startInt <= endInt) {
return [startInt, endInt];
} else {
return [endInt, startInt];
}
}
/** /**
* Return true if the page number `label` is within `range`. * Return true if the page number `label` is within `range`.
* *
* Returns `false` if the label is outside `range` or the relation between the
* label and the range could not be determined.
*
* @param label - A page number such as "10", "iv" * @param label - A page number such as "10", "iv"
* @param range - A page range expressed as a single page number, or a hyphen * @param range - A page range expressed as a single page number, or a hyphen
* separated range (eg. "10-12"). Page ranges are inclusive, so the page * separated range (eg. "10-12"). Page ranges are inclusive, so the page
...@@ -8,29 +55,54 @@ ...@@ -8,29 +55,54 @@
* specify an empty range. * specify an empty range.
*/ */
export function pageLabelInRange(label: string, range: string): boolean { export function pageLabelInRange(label: string, range: string): boolean {
if (!range.includes('-')) { return pageRangesOverlap(label, range) === true;
return label === range; }
}
let [start, end] = range.split('-'); /** Convert an open range into an integer range. */
if (!start) { function normalizeRange(
start = label; range: [number | null, number | null],
} min: number,
if (!end) { max: number,
end = label; ): [number, number] {
return [range[0] ?? min, range[1] ?? max];
}
/**
* Return true if two page ranges overlap.
*
* Each range may be specified as a single page number, or a hyphen-separated
* range.
*
* Returns true if the ranges overlap, false if the ranges do not overlap, or
* `null` if the relation could not be determined.
*/
export function pageRangesOverlap(
rangeA: string,
rangeB: string,
): boolean | null {
const intRangeA = parseRange(rangeA);
const intRangeB = parseRange(rangeB);
if (!intRangeA || !intRangeB) {
if (rangeA && rangeB && rangeA === rangeB) {
// As a special case for non-numeric ranges, we consider them overlapping
// if both are equal. This means `pageRangesOverlap("iv", "iv")` is true
// for example.
return true;
}
// We could not determine whether the ranges overlap.
return null;
} }
const [startInt, endInt, labelInt] = [
parseInt(start), const minPage = 1;
parseInt(end), const maxPage = 2 ** 31;
parseInt(label), const [aStart, aEnd] = normalizeRange(intRangeA, minPage, maxPage);
]; const [bStart, bEnd] = normalizeRange(intRangeB, minPage, maxPage);
if (
Number.isInteger(startInt) && if (aStart <= bStart) {
Number.isInteger(endInt) && return bStart <= aEnd;
Number.isInteger(labelInt)
) {
return labelInt >= startInt && labelInt <= endInt;
} else { } else {
return false; return aStart <= bEnd;
} }
} }
import { pageLabelInRange } from '../page-range'; import { pageLabelInRange, pageRangesOverlap } from '../page-range';
describe('pageLabelInRange', () => { describe('pageLabelInRange', () => {
[ [
...@@ -94,3 +94,80 @@ describe('pageLabelInRange', () => { ...@@ -94,3 +94,80 @@ describe('pageLabelInRange', () => {
}); });
}); });
}); });
describe('pageRangesOverlap', () => {
[
// Matching single page
{
rangeA: '1',
rangeB: '1',
overlap: true,
},
// Non-matching single page
{
rangeA: '1',
rangeB: '2',
overlap: false,
},
// Overlapping numeric ranges
{
rangeA: '1-2',
rangeB: '2-4',
overlap: true,
},
{
rangeA: '2-4',
rangeB: '1-2',
overlap: true,
},
// Inverted numeric ranges. These are implicitly normalized.
{
rangeA: '2-1',
rangeB: '4-2',
overlap: true,
},
// Half-open ranges
{
rangeA: '1-',
rangeB: '-3',
overlap: true,
},
// Non-overlapping numeric ranges
{
rangeA: '1-2',
rangeB: '3-4',
overlap: false,
},
{
rangeA: '3-4',
rangeB: '1-2',
overlap: false,
},
// Relation is undefined if either of the ranges is non-numeric.
{
rangeA: 'ii',
rangeB: '3-4',
overlap: null,
},
{
rangeA: 'i-iii',
rangeB: 'iii-iv',
overlap: null,
},
{
rangeA: '1-ii',
rangeB: 'ii-3',
overlap: null,
},
// As a special case, matching non-numeric ranges overlap.
{
rangeA: 'ii',
rangeB: 'ii',
overlap: true,
},
].forEach(({ rangeA, rangeB, overlap }) => {
it('returns true if the two ranges overlap', () => {
assert.strictEqual(pageRangesOverlap(rangeA, rangeB), overlap);
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment