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`.
*
* 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 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
......@@ -8,29 +55,54 @@
* specify an empty range.
*/
export function pageLabelInRange(label: string, range: string): boolean {
if (!range.includes('-')) {
return label === range;
}
let [start, end] = range.split('-');
if (!start) {
start = label;
}
if (!end) {
end = label;
}
const [startInt, endInt, labelInt] = [
parseInt(start),
parseInt(end),
parseInt(label),
];
if (
Number.isInteger(startInt) &&
Number.isInteger(endInt) &&
Number.isInteger(labelInt)
) {
return labelInt >= startInt && labelInt <= endInt;
return pageRangesOverlap(label, range) === true;
}
/** Convert an open range into an integer range. */
function normalizeRange(
range: [number | null, number | null],
min: number,
max: number,
): [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 minPage = 1;
const maxPage = 2 ** 31;
const [aStart, aEnd] = normalizeRange(intRangeA, minPage, maxPage);
const [bStart, bEnd] = normalizeRange(intRangeB, minPage, maxPage);
if (aStart <= bStart) {
return bStart <= aEnd;
} else {
return false;
return aStart <= bEnd;
}
}
import { pageLabelInRange } from '../page-range';
import { pageLabelInRange, pageRangesOverlap } from '../page-range';
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